目录
一、SQL盲注
二、身份认证逻辑
三、条件竞争
突然发现,笔者接触代码审计工作也有一年半的时间了,经历的大大小小的代码审计项目百余项,但是并没有好好的研究过关于python的代码审计内容,或者说python有什么需要审计的呢?fortify拓展插件貌似也支持python审计,至少原版我没成功过。
但,前不久一个小伙伴还是问了我python代码审计的问题,code终究还是code,万变不离其宗,今天笔者借朋友之前写的几个小实例抛砖引玉下,简单涉及下这一冷门领域。
一、SQL盲注
sql注入这个东西吧,稍微有点安全开发的常识就不会出现,我们简单来看下python中的sql注入是什么个样子。
这是一个查询系统的马卡龙配色查询表单:
这是传入后端的post表单。
当我尝试进行黑盒SQL注入测试的时候,前端返回了500的错误,这个时候从事黑盒渗透的小伙伴们会有什么想法?放弃or继续?
我们来看下后台debug:
当前查询内容为:rabbit'
MySQLdb._exceptions.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for t
he right syntax to use near ''' at line 1")
唉,我这暴脾气,这种绝对意义上的盲注,sqlmap尝试了两次败下阵来,对于黑盒测试工作来讲,差不多可以去摸鱼了。
然而,对于一个白盒来讲,我们必须相信眼前看到的!
顺着username
这个参数几经周折,我们终于找到了它的调用:
sql = ("SELECT weight FROM `regmeek` WHERE username={}".format(username))
mycursor.execute(sql)
涉及到了数据库查询中一个权重的字段,这个字段并不会反馈给前端,仅用于后端权重记录,所以造成了这个SQL盲注,而且因为返回结果报错,导致最终服务器并未做出正确响应,返回500报错,某种意义上讲,就算是漏洞也完全无法利用。
然而OOB(数据外带攻击)正是为此而生,通过DNSlog外带,依然可以拿到注入结果,时间关系不做展开,有兴趣的小伙伴可回顾之前的SQL注入OOB教程。
最后,来说下python的SQL注入修复方案,大同小异:
1、使用ORM,即尽可能不要直接拼接SQL语句。
2、验证输入类型和输入内容,即filter过滤器。
3、转义特殊字符,如引号、分号等。
4、参数化查询,各种接口库为我们自动转义。
笔者的防sql注入习惯正是参数化查询,我们在此给出修复后的code:
sql = ("SELECT weight FROM `regmeek` WHERE username=%s")
sqlV=[username]
mycursor.execute(sql,sqlV)
大家可以跟上面的漏洞代码简单做下对比,当然同样推荐ORM,有机会给大家介绍,emmmm,放到Scrapy章节吧,简单预告下。
二、身份认证逻辑
逻辑漏洞是最为有趣的漏洞,基本无法为漏扫软件所发现,这就意味这小白帽需要更加细心一些,我们本次以一个flask的小demo来错误示范下。
我们来简单看下这货的身份验证逻辑:
app.config['SECRET_KEY'] = 'rabbitmask'
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.session_protection='strong'
#session_protection 能够更好的防止恶意用户篡改 cookies, 当发现 cookies 被篡改时, 该用户的 session 对象会被立即删除, 导致强制重新登录.
login_manager.login_view = 'login'
#指定了登录页面的视图函数
login_manager.login_message = 'Welcome To AboutU!'
#指定了提供用户登录的提示信息
@login_manager.user_loader
def load_user(username):
if userget(username) is not None:
curr_user = UserMixin()
curr_user.id = username
return curr_user
可以看到也是做足了细节,所有系统内界面均做足了权限认证,基本不存在未授权访问等问题。
然而有个set-cookie细节引起了我的注意:
if usercheck(username,password):
curr_user = UserMixin()
curr_user.id = username
login_user(curr_user)
resp = make_response(render_template('AboutU.html',username=request.cookies.get('username')))
resp.set_cookie('username', username)
return resp
请求:
POST /login HTTP/1.1
Host: 192.168.1.254:1988
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Connection: close
Referer: http://192.168.1.254:1988/
Upgrade-Insecure-Requests: 1
username=admin&password=admin
响应:Set-Cookie: username=admin;
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1484
Set-Cookie: username=admin; Path=/
Set-Cookie: session=.eJwlzrsRgzAMANBdVFPItrAsluH084UiKSBUueyeIm-C94F9nnk9YHufdy6wHwEbMFImVVQfbJph2FsxqlwaU9gsNafM1RtrDGfuTRllcJtrzRwi3ZOrhHDn7LUEOmKihgW7UYZ6k1WmdQwcSk6dCHGkK1rhAgvcV57_jMbzeMH3B272MRE.XZFsPg.Of_mJb4c4nhcBBRwjIqouJFSo4U; HttpOnly; Path=/
Server: Werkzeug/0.11.15 Python/3.7.2
Date: Mon, 30 Sep 2019 02:45:18 GMT
我们再来看下管理后台的身份验证,跟前端不同的地方在于通过isadmin()
判断是否为管理员然后usercheck()
判断身份,这些都没问题,还有一处差异是set-cookie多附了一个值:resp.set_cookie('isadmin', '1')
if isadmin(username) and usercheck(username,password):
curr_user = UserMixin()
curr_user.id = username
login_user(curr_user)
re = usermanager.usersearchall()
resp = make_response(render_template('adminconsole.html',usersearch=re))
resp.set_cookie('username', username)
resp.set_cookie('isadmin', '1')
return resp
我们看下系统对于越权的验证机制:
if request.cookies.get('isadmin')=='1':
re = usermanager.usersearchall()
return render_template('adminconsole.html',usersearch=re)
else:
abort(403)
/摊手手,这就凉了呀,对于权限的把控居然依赖的是cookie中的isadmin
参数,的确,对于黑盒测试的普通用户来讲,是完全察觉不到这个参数的,但白盒自然了如指掌了。
登录前台访问后台返回403:
然而我们在cookie中手动加入isadmin
参数,身份认证绕过成功:
修复方案:
不要依赖前端数据来做身份管理,用户提交的信息一个标点符号也别信!
这里的修复就是将isadmin
参数转移到数据库中,身份认证通过查询该用户在数据库中的isadmin
字段值是否为1
来进行身份校验,而不是依赖cookie中的参数识别。
三、条件竞争
朋友的系统中调用了个fofa的接口,采用了多进程并发,然而多进程并发做数据库写入还好,如果是写文件将会造成条件竞争的问题,我们来简单看下核心代码:
def getinfo(p,q):
response=requests.get("https://fofa.so/result?full=true&page="+ str(p) +"&qbase64="+str(q),headers=headers)
r1= re.compile(r'.*?>>>>\n请查看当前路径下文件:ip.txt')
瞅瞅这破格式,当频繁的做文件打开、写入、关闭操作,会导致进程之间相互影响,一个进程还没写入完毕,另一个进程就强行结束了文件流,最终造成了如下破格式:
这种情况下如何修复呢?相比大家第一个想到的是进程锁,说白了就是一个进程写入时,其它进程暂时等待,这势必会影响执行效率,所以再次推荐另一个方案,异步并发,使用callback来完成,修复如下。
def getinfo(p,q,d):
print('当前进度:第{}页'.format(p))
response=requests.get("https://fofa.so/result?full=true&page="+ str(p) +"&qbase64="+str(q),headers=headers)
r1= re.compile(r'.*?>>>>\n请查看当前路径下文件:ip.txt')
异步并发
在这里我们用到的技术就是异步并发,多进程并发做网络请求,文件写入由callback异步完成,防止条件竞争漏洞的形成,也通过这个小实例告诉大家,不要无脑lock,那样会失去并发的意义。
END
最后祝大家国庆快乐,有时间还请多陪陪父母,也许工作了你才会明白,跟父母相处的时间是多么少。真的不需要你做什么,待在他们视野里,聊点有的没的什么都好,只要你在,共勉。