QL教程2-找到小偷
前言
在我们使用codeql
进行代码审计之前,不妨先学习一些QL
的基础语法,磨刀不误砍柴工。
官方教程链接:https://codeql.github.com/docs/writing-codeql-queries/ql-tutorials/
在这个教程中我们作为一个侦探,为了解决遇到的问题使用QL
进行相应的调查
寻找小偷
我们作为侦探在虚拟的村庄中寻找小偷,在这个过程中我们学习使用逻辑连接词,量词和聚合函数
有一个小村庄隐藏在深山中,村子分为东南西北四个部分,中央矗立着一座黑暗而神秘的城堡。城堡内,在最高的塔楼里,锁着国王珍贵的金冠。一天晚上,发生了一起可怕的罪行,一个小偷闯入塔楼并偷走了王冠!
你知道小偷一定是村里的人,因为只有村里的人知道王冠,经过一些专业的侦探工作,你将会获得村里所有人的名单以及他们的一些个人详细信息。
尽管如此,你还是不知道是谁偷走了王冠,所以你在村子里走来走去寻找线索,村民们的行为非常可疑,您确信他们掌握了有关小偷的信息。但他们拒绝直接与您分享他们的知识,只会通过你的提问来回答"是"或者"不是"
如果大家有玩过剧本杀的话,可以发现有点像剧本杀里面的海龟汤
,出题人只回答是,不是两个答案 😃,我们通过提问来构建整个故事的脉络
作为侦探,你开始提出一些创造性的问题并记录下答案,以便稍后将它们与你后面得到的信息进行比较
需要手动搜索的信息太多,因此您决定使用新获得的 QL 技能来帮助您进行调查……
我们定义了许多的QL谓词来帮助你从表中提取数据,QL谓词是一个小型查询,它表达了各种数据之间的关系并描述了它们之间的一些属性。在这种情况下,谓词为你提供有关一个人的信息,例如他们的身高或者年龄。
QL官方将这些谓词存储在 QL 库tutorial.qll
中。要访问此库,请在VScode中输入import tutorial
库用来存储常用谓词,这也是方便代码的复用,就像是写python代码的时候import一样。方便我们不必在每次需要时都定义谓词。
例如,使用t.getHeight()返回t的高度。
接下来我们开始寻找小偷
- 村民对
小偷身高超过150厘米吗?
的回答是
。要使用此信息,您可以编写以下查询来列出所有身高超过 150 厘米的村民。这些都是可能的嫌疑人。
import tutorial
from Person t
where t.getHeight()>150
select t
接着我们使用逻辑连接词来编写更加复杂的查询,以便组合不同的信息
- 例如,如果您知道小偷 30 岁以上并且有一头棕色头发,您可以使用以下where子句来链接两个谓词
import tutorial
from Person t
where t.getAge()>30 and t.getHairColor()="brown"
select t
- 如果小偷不住在城堡的北边
import tutorial
from Person t
where not t.getLocation() = "north"
select t
- 如果小偷有棕色头发或黑色头发
where t.getHairColor() = "brown" or t.getHairColor() = "black"
- 您还可以将这些连接词组合成更长的语句
where t.getAge() > 30
and (t.getHairColor() = "brown" or t.getHairColor() = "black")
and not t.getLocation() = "north"
这里需要注意的是,没有括号的话,and
的优先级高于or
,所以我们需要使用括号来保证查询按照我们的预期进行
然而谓词并不总是只返回一个值。例如,如果一个人的黑色头发变成灰色了,p.getHairColor()将返回两个值:黑色和灰色。
小偷秃了怎么办?在这种情况下,小偷没有头发,所以getHairColor()谓词根本不返回任何结果!
如果你知道小偷肯定不是秃子,那么一定有一种颜色与小偷的头发颜色相匹配。在 QL 中表达这一点的一种方法是引入一个新的string类型变量 c ,并选择那些t与t.getHairColor()的值匹配的变量c。
from Person t, string c
where t.getHairColor() = c
select t
这里需要注意的是,我们只是引入了临时变量c,在后面where子句中就不再需要用到它了,在这种情况下,我们最好使用exists
from Person t
where exists(string c | t.getHairColor() = c)
select t
exists
引入了一个string类型的临时变量c,并且只有至少有一个满足string c | t.getHairColor() = c
条件
现在我们回到追查小偷的主线任务上,用刚才我们学到的方法,编写一个查询来查找满足前八个问题答案的人
import tutorial
from Person t
where
t.getHeight()>150 and
not t.getHairColor()="blond" and
exists( string s | t.getHairColor()=s ) and
not t.getAge()<30 and
t.getLocation()="east" and
(t.getHairColor()="black" or t.getHairColor()="brown") and
not (t.getHeight()>180 and t.getHeight()<190) and
exists(Person temp | t.getAge()<temp.getAge())
select t
可以看到尽管如此我们还是没有找到小偷
不过范围已经大大缩小了,我们离解开谜团已经越来越近了,想要在嫌疑人名单中找出谁是真正的小偷,我们必须收集更多的信息并在下一步中完善查询。
如果我们想要找到村里面最老,最年轻,最高或者最矮的人应该怎么办?在上面我们知道可以使用exists
。实际上我们还可以用max
或者min
来执行该操作。
通常,聚合是对多条数据执行操作并返回单个值作为其输出的函数,常见的聚合是count
,max
,min
,avg
和sum
,使用聚合的一般方法是:
<aggregate>(<variable declarations> | <logical formula> | <expression>)
例如,使用max
聚合来查找村里面最年长的人的年龄:
import tutorial
select max(int i | exists(Person p | p.getAge()=i ) | i)
可以得到最年长的是的年龄是97岁
,在这个聚合中考虑所有的整数i,并且限制i为与村里人年龄相匹配的值,然后返回最大的匹配整数
但是我们应当如何在实际查询中使用它呢?
如果小偷是村里最年长的人,那么你知道小偷的年龄等于村民的最大年龄,这样就可以构建查询为:
import tutorial
from Person t
where t.getAge()=max(int i | exists(Person p | p.getAge()=i ) | i)
select t
这时得到结果为Susannah
但是这样的过程缺点也很明显,即通用的聚合语法很长而且很不方便。在大多数情况下,我们可以省略聚合的某些部分,一个特别有用的QL特性是有序聚合
,这允许我们使用order by
对表达式进行排序
例如,我们使用order by
有序聚合选择最老的村民
import tutorial
select max(Person p | | p order by p.getAge())
接下来是更多关于聚合的案例
- 村东最矮的人
min(Person p | p.getLocation() = "east" | p order by p.getHeight())
- 村南人口数
count(Person p | p.getLocation() = "south" | p)
- 村民平均身高
avg(Person p | | p.getHeight())
- 所有棕色头发村民的年龄总和
sum(Person p | p.getHairColor() = "brown" | p.getAge())
学习了这些之后,我们就可以出发抓住罪魁祸首了,将剩余的问题使用QL进行查询:
import tutorial
from Person t
where not t=max(Person temp | | temp order by temp.getHeight())
and t.getHeight()<avg(float i | exists(Person temp | temp.getHeight() = i) | i)
and t=max(Person temp | temp.getLocation()="east"| temp order by temp.getAge())
select t
事实上,即便不加上前面的八个查询,我们也能够得到最后小偷的身份,Hester
,他这是那个小偷!
END
建了一个微信的安全交流群,欢迎添加我微信备注进群
,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注 😃