Building off of my last blog post, I could start two containers – ZAP and the application and then run ZAP against the application and generate a report. However, I observed that this was not getting me good results. Most of the issues reported were missing header issues which weren’t that useful to begin with.
So, I had to figure out a way to make this more meaningful and effective if we were to actively deploy such an automated scanning process in our DevOps pipeline.
The way I figured this would work was to get some legitimate traffic in ZAP before beginning the scan. And, the way we would get any traffic inside ZAP would be to use it as a proxy first before the actual scanning. Now, this would mean that the application would have to be browsed manually in order to generate the data. But, we were trying to automate this entire process so any form of manual intervention was not desired, at least till the point when the reports need to be triaged.
Luckily, we had some custom test cases written in Python and Lua that would generate and send some API requests to the application. So, I had to simply do that first (send the test cases to the application proxying it via ZAP) before beginning my ZAP scan. The results of doing this were slightly better. It also meant it took care of the authentication part because the requests that were sent as a result of running the test cases also contained some headers that were used to authenticate to the application so I didn’t have to change anything specifically within the ZAP API code.
We got a few more issues than just the header stuff so I was happy the approach worked. There is still a lot of work to be done but since the application I am dealing with is not a traditional web application, I am looking at a slightly different route now and maybe do something more than just running the ZAP scan. More on that later as and when I have something to blog about.
A few more things I added to this process were that the reports are now being automatically sent to our JIRA instance and a ticket gets created with the reports as attachments. We also have a webhook for Slack built in so whenever this ticket is created, we get a nice notification in our Slack channel notifying everyone that the scan was run and the report was generated attached to the JIRA ticket for auditing purposes. I also added some exception handling and cleaned the code a little bit.
Overall, the complete automated process looks like:
zaprun.sh can be found here.
runzap.py can be found here.
jiraconnect.sh can be found here. For this script, you will also need to create a folder called “data” in $pwd and then add 2 files in that directory – credentials.json and data.json. Credentials.json will have your username and password to authenticate to JIRA. It will look something like this:
Data.json will have the ID of your JIRA project, summary, description, issue type and label for the issue that will get created. This information can easily be obtained from your JIRA installation using the REST API browser plugin in JIRA. It will look something like this:
“summary”: “ZAP Scan Result”,
“description”: “This issue contains the scan results when ZAP is run against the app”,
And, that’s it! A fully automated process of running OWASP ZAP in your Devops build pipeline with test cases being proxied via ZAP inside Docker containers and reporting in JIRA with notifications sent to Slack.
Feel free to reach out to me if you have any questions or just to share your experiences if you have been trying to do something similar.
To start of, this has been a lot of fun learning experience for me. It had been a while since I did any sort of Bash/Python scripting so it definitely got me back on track. Also, there are some resources out there but nothing helped me as such for the particular case I was looking to solve. Lastly, I will try to convert this into a series of blog posts where I will try to get much deeper with ZAP scanning and reports, integration with CI build servers, etc. as and when time permits. And, btw, you would need to know the basics of working with Docker in order to understand this blog. Having said that, I will try to explain most of the commands I have in my scripts. So, lets begin.
TL;DR of this post:
1. You install Docker.
2. You run a custom bash script.
3. This bash script starts 2 Docker containers from 2 different images:
- One is a sample web application. The name of the image from where this container is built is “training/webapp”. This can be found on Docker Hub.
- The other container is built from a custom image which in turn is built on top of “owasp/zap2docker-stable” image found on Docker Hub.
4. Once, both the containers are started, ZAP runs against the web app. It does a very basic spidering and scanning. Once everything is done, the report is stored on the ZAP container.
5. The report is then transferred onto the host and all the containers are deleted.
So, basically, you ran ZAP against a web app and generated a XML report on your file system – all automated by just one script!
The main bash script (zaprun.sh) mentioned in point 2 above is as follows. I have left comments above each command so it should be self-explanatory. Try to understand this script and save it for now. We will run this later on:
#Running the sample webapp and storing the ID in a variable
WEBCONTAINERID=$(docker run -d -P –name web training/webapp python app.py)
echo Container ID = $WEBCONTAINERID
#Inspecting the above container to gather its IP address and port that will be accessible to the ZAP container to run the scan against
WEBDOCKERIP=$(docker inspect $WEBCONTAINERID | grep -w IPAddress | sed ‘s/.*IPAddress”: “//’ | sed ‘s/”,$//’)
echo Webapp Docker IP = $WEBDOCKERIP
WEBDOCKERPORT=$(docker port $WEBCONTAINERID | sed ‘s/\/tcp.*//’)
echo Webapp Docker Port = $WEBDOCKERPORT
#Running the ZAP container. Notice that this container is named test and there is a custom python script runzap.py that is run. I will provide these later on. This is what I built on top of owasp/zap2docker-stable image
ZAPCONTAINERID=$(docker run -d –name zap test python /zap/ZAP_2.4.0/runzap.py http://$WEBDOCKERIP:$WEBDOCKERPORT)
echo ZAP Container ID = $ZAPCONTAINERID
#Inspecting the above container to see whether it is running or not. If it is not running, that means ZAP has finished the scan and the report is generated.
STATUS=$(docker inspect $ZAPCONTAINERID | grep Running | sed ‘s/”Running”://’ | sed ‘s/,//’)
while [ “$flag” = “1” ]; do
if [ $STATUS == “true” ];
echo ZAP is running..
STATUS=$(docker inspect $ZAPCONTAINERID | grep Running | sed ‘s/”Running”://’ | sed ‘s/,//’)
echo ZAP has stopped
STATUS=$(docker inspect $ZAPCONTAINERID | grep Running | sed ‘s/”Running”://’ | sed ‘s/,//’)
#Copying the report to Host OS
echo Copying the report to host in the current directory with the name report.xml
docker cp $ZAPCONTAINERID:/zap/ZAP_2.4.0/report.xml .
#Deleting all the containers that were created as a result of this script
echo Deleting the ZAP Container
docker rm $ZAPCONTAINERID
if [ $? -eq 0 ]
echo Stopping the Webapp Container
docker stop $WEBCONTAINERID
if [ $? -eq 0 ]
echo Deleting the Webapp Container
docker rm $WEBCONTAINERID
Now, on your host OS, create a folder and paste the following 2 files in that folder:
- Dockerfile (self-explanatory)
MAINTAINER Anshuman Bhartiya <firstname.lastname@example.org>
RUN apt-get update && apt-get install -y \
RUN pip install python-owasp-zap-v2
ADD runzap.py /zap/ZAP_2.4.0/
from pprint import pprint
from zapv2 import ZAPv2
#Starting ZAP as a daemon on port 8090
print ‘Starting ZAP …’
subprocess.Popen([“zap.sh”,”-daemon”,”-port 8090″,”-host 0.0.0.0″],stdout=open(os.devnull,’w’))
print ‘Waiting for ZAP to load, 10 seconds …’
#Taking the IP address to scan against through the command line. This is where you will provide the value for http://$WEBDOCKERIP:$WEBDOCKERPORT in the above bash script
target = sys.argv
zap = ZAPv2()
print ‘Accessing target %s’ % target
#Spidering the target
print ‘Spidering target %s’ % target
while (int(zap.spider.status) < 100):
print ‘Spider progress %: ‘ + zap.spider.status
print ‘Spider completed’
#Scanning the target
print ‘Scanning target %s’ % target
while (int(zap.ascan.status) < 100):
print ‘Scan progress %: ‘ + zap.ascan.status
print ‘Scan completed’
#Printing the XMLreport and saving it on the file system of the ZAP container at /zap/ZAP_2.4.0/report.xml which we will later copy to the Host OS
with open(“/zap/ZAP_2.4.0/report.xml”, “w”) as f:
That’s all you need pretty much. 3 files –
- The main bash script. I have uploaded this here as well.
- Dockerfile to build the custom image and container. I have uploaded this here as well.
- runzap.py script that is used to start the ZAP daemon and run it against an IP. I have uploaded this here as well.
Once you have all these files, navigate to the folder you created on your host OS and build the Dockerfile with the following command:
docker build -t test .
After this file is built, type:
you should now see the test image being listed in your repository.
And, to save some time, download the other container as well with the following command:
docker pull training/webapp
In the end, you should see 3 docker images in your repository:
You should be good to go now to run the main bash script. So, just enter:
Hopefully, everything goes fine and you will see the report.xml file in your current directory.
This technique can be used to bypass CSRF protections in some applications by using a static CSRF token (for all users of that application) that looks like a specific format string.
So, to begin with, have you ever noticed CSRF tokens being something like this:
If you have, have you ever looked at it more closely? The above string is basically divided into 3 parts separated by some delimiters. In the above example, the first part “RHAU3cgmTvWy6RWSj” and the second part “NdJy2v8y8Z0g2U5qTQg4ap” are separated by the delimiter “+”. The second part “NdJy2v8y8Z0g2U5qTQg4ap” and the third part “lqeLEfA” are separated by the delimiter “/”. The string finally ends with “==”.
So, considering the above example, if you encounter an application that uses CSRF tokens as shown above, try fiddling with the actual string making sure you keep the format consistent i.e. 3 parts separated by some delimiters and so on and so forth.
In my case, the format ended up being “xxxxxxxxxxxxxxxxxxxxxxxxx%2Bxxxxxxxxxxxxxxxxxxxxxxxxxx%2Fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxw%3D%3D” which when decoded is “xxxxxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxw==”
The length of the above string (or the number of x’s) would depend on different applications. So, consider this as just a PoC.
In a nutshell, what I observed was that an attacker can just trick a victim in order to submit a POST request with the above string as the csrf token as a POST parameter and the application server would gladly accept it because it was only looking to ensure the tokens met a specific format and didn’t really compare the actual value received to the value stored on the server side. As a result of this, I was able to bypass CSRF protections throughout the entire application.
There are some more nuances to the above scenario as well. Consider the case of a double-submit CSRF protection. What that entails is that the CSRF tokens need to be sent in two places – one as a session cookie and one in the POST body. Or, maybe one as a custom header and one in the session cookie. Or, maybe one as a custom header and one in the POST body. There can be multiple possible combinations.
The jist is that they both need to be the same. This is mostly done to prevent the headache of storing the CSRF values on the server side. In such cases, bypassing the protection is not easy because as an attacker, you don’t really have any control over a victim’s browser to be able to set custom headers or session cookies. The most you can do is to trick a victim in order to submit a malicious POST request. But, since the browser sends the headers and/or cookies automatically, the chances of those values matching your value in the POST request are negligible. Hence, the protection, if implemented properly, can be quite effective.
But, when you consider the example discussed above, it was observed that even though the browser was sending a custom header and/or session cookie automagically along with the attacker tricked value “xxxxxxxxxxxxxxxxxxxxxxxxx%2Bxxxxxxxxxxxxxxxxxxxxxxxxxx%2Fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxw%3D%3D” in the POST body, the server was only looking to ensure that the format matched and not the actual values. So, again, this was a complete CSRF protection bypass because it didn’t matter what CSRF values the browser was sending (as headers and/or cookies) as long as an attacker could trick a victim to submit a POST request with the above static CSRF string.
I am not sure if this technique was already known. If it was, pardon my ignorance. I found this during testing and thought it was pretty interesting hence this post.
Authenticating to an account on the Indeed iPhone app and then changing the country triggers the user to logout (at least it appears to log out a user). The country changes just fine but instead of the user still being logged in, the “Sign In” option appears in the application. When the user clicks on this “Sign In” option, a set of requests are sent to the server which automatically logs the user back in (obviously because the user never logged out in the first place. The user just changed the country).
Within these URLs that are sent out, there is one particular request that gets sent to the “/account/checklogin” endpoint with the value “passrx” over HTTP. What this means is that a MiTM attacker can easily retrieve this URL over the network.
The attacker can then use the captured URL to take over the victim’s account completely.
It should also be noted that this is not only an account hijacking vulnerability but also a login CSRF vulnerability. An attacker can easily capture the above request for his own account and then trick a victim to login that account.
But, as it is obvious, the more serious vulnerability here is the account hijacking vulnerability by a MiTM attacker.
A PoC video demonstrating the vulnerability is here.
This vulnerability was reported via Bugcrowd to the Indeed bug bounty program and this issue was deemed as a duplicate. I then got explicit permission from the program owners to disclose this publicly.
I reported a bug to Slack via HackerOne on December 13, 2014. Slack closed it as N/A. Considering it was N/A, I went ahead and blogged about it here on December 18, 2014. I gave them a heads up as well on the submission at HackerOne that I will be disclosing it before I actually disclosed it. They kept radio silence so I assumed they didn’t have any issues. They never said not to disclose or anything like that which would make sense because it was marked as N/A meaning they are not interested in the bug in the first place.
Around the same time or rather a day earlier on December 12, 2014, I had reported another bug to Slack via HackerOne. And, they closed it as Duplicate. The entire submission along with the conversations can be found here. In a nutshell, they wanted to do a coordinated disclosure once the issue was fixed. I was perfectly fine with it. I completely understand and respect the ethics of a bug bounty program and I agreed to that. But, after that, there was complete radio silence. I tried following up multiple times but nobody cared to respond or update me regarding the fix as is evident from the document. I also left a comment (1 month and then 4 days before the disclosure) that if I don’t hear back with any update or anything, I would go ahead and disclose it 90 days after the initial submission. According to industry standards, that seems to be the trend these days so I chose to stick with it. I finally disclosed it here on this blog.
On March 12, 2015, I reported yet another bug to Slack, again via the HackerOne platform. This bug was closed today March 16, 2015 as N/A without any explanation or reasoning. The entire conversation along with the bug submission can be found here. Consider this document as a public disclosure for this bug since it is marked and closed as N/A and they don’t seem to be interested in it anyways.
As evident from the latest bug submission document, I have been told that I have “gone against the spirit of a bug bounty program by disclosing things without consent”. They feel that for the second bug described above, “the disclosure is owned by the original reporter.” and, that “By disclosing this without coordinating” I have stolen “the original reporter’s opportunity to disclose a finding.” They have apparently spoken to HackerOne last week and asked to remove me from participating in their bug bounty program. I was apparently supposed to receive some communication regarding this (which btw I never did).
I am honestly very disappointed with how things have been handled. I personally don’t think I did anything against the spirit of a bug bounty program. I am all for coordinated disclosure but if the program owners fail to coordinate or communicate in a timely manner, there is no such thing called coordinated disclosure. Combined with their responses on all my bug submissions and their decision to ban me from participating in their bug bounty program, this is probably the worst experience I have had so far and I feel this is a perfect example of how not to operate a bug bounty program.
I would love to get some feedback and thoughts on this. I am open to criticism and improving anything that I could have done better from my side to make this less painful.
When I register for a Slack team from the Safari browser in my iPhone, the final request for registering a team looks like:
The response to this request is a redirect to the URL
https://slack.com/checkcookie?redir=https%3A%2F%2Fn00bgiri.slack.com%2F%3Ffresh which is then redirected to
https://n00bgiri.slack.com/?fresh which is then redirected to
https://n00bgiri.slack.com/app. The series of these requests/responses can be seen below:
The final response for the request
https://n00bgiri.slack.com/app looks like:
This screenshot is taken from the Safari browser in the iPhone.
An important thing to notice here is the option “Open Slack”. That is actually a hyperlink that looks like:
<a href="slack://login/<redacted>/xoxo-<redacted>-<redacted>" class="btn btn-primary btn-large">Open Slack</a>
The value xoxo-<redacted>-<redacted> in the above URL is the keys to the kingdom. It can be essentially considered as a replacement for the username/password combination. It is a static value that does not change or get invalidated even if the account is logged out. This brings us to the first issue i.e. If an attacker gets hold of this value of a victim (by different attack vectors which is out of scope for the purposes of this discussion), he can essentially gain complete access of a victim’s account perpetually. It does not matter if the victim is logged in or not since it is a static token and does not get invalidated on logout. Please note that the above value should not be confused with another token value that looks similar but is of the form xoxs-<redacted>-<redacted>-<redacted>-<redacted>. I will describe what this other value is in a moment. The xoxo value is only created/sent in the response once when the team is first registered so that’s important to know here.
Now, the normal authentication flow in the Slack iOS app is something like below:
- The first authentication request is sent to the URL
https://slack.com/api/auth.signinwith the POST parameters
- In response to the above request, the server assigns and sends a token (xoxs-<redacted>-<redacted>-<redacted>-<redacted>) in the JSON response.
- Then, a request is sent to the URL
https://slack.com/api/users.loginwith the POST parameters
token. The token sent here is the xoxs token received above. This completes the authentication flow.
- The xoxs token is then used in all subsequent requests.
Now, if you logout of the iOS application, this xoxs token gets invalidated (as it should be) but the static xoxo token discussed earlier does not. And, that’s the problem.
This brings me to the second attack aka Login CSRF:
Normally, in a Login CSRF attack, an attacker tricks a victim to submit an authentication request with the username and password as parameters in the request. If there are no CSRF tokens present in this request, it becomes possible to trick victims to authenticate to an attacker controlled account.
So, we now know that the xoxo token is a static token and can be treated as username/password. Therefore, the authentication request would look something like this:
Notice there is nothing that can be considered as a CSRF token in the above request.
I have created a video PoC for this attack as well.
Exploiting the Login CSRF is extremely easy in this case.
What I essentially did was that as an attacker, I noted down the hyperlink that the server sent when I first registered my team:
I, then sent, the victim an email with this link above as a hyperlink. When the victim clicks on that, the Slack iOS app opens up and sends the above authentication request automatically. I didn’t even have to craft a HTML that sends a POST request to the
/api/users.login endpoint. It was as simple as tricking the victim to click on a GET URL. The Slack app does all the leg work for the attackers.
So, that’s if folks. To summarize, I described 2 issues above:
- Static tokens that don’t get invalidated
- Login CSRF
I am not an expert in iOS pentesting but I googled the correct way to handle iOS URL schemes and I saw these websites:
I think they are worth looking into. The premise is essentially that, you should be asking the victim user before opening up the
slack:// URL automatically in the Slack application to mitigate the Login CSRF issue.
For the static token issue, I think it’s a bad idea to associate static tokens with user accounts all together. So, that should be looked into as well and tried to get rid of. If not, I don’t see any reason of sending that value in the response in clear text after registering.
Thanks for your extensive report. Both of these issues are already known and being fixed.
1) The static tokens are something we are moving away from for all apps, including iOS. We hope to have this completed soon.
2) There is not much security implication of logging a user in this way. Because Slack groups are closed to the public, it would be difficult to convince the user they are in the correct group if you manage to log them in. We have an open bug to add CSRF to the login page,but this is low priority.
The bug that I am going to describe here was actually discovered accidentally while I was checking my privacy settings in Facebook. And, it is so simple that one doesn’t need to be technical at all to find it. It could have been discovered by anybody (literally). I guess I just got lucky and the fact that I have been a Facebook user since 2007 aided in the discovery as well. But, the bottom line is that you just need to be looking at the right place at the right time to earn bounties from the various bug bounty programs out there.
Anyways, let’s get to the bug now.
Privacy Violation Bug#1
This bug allowed disclosure of “parents” information (to the public) of some Facebook users inspite of the privacy settings being explicitly set to not allow that information to be viewed by the public or friends. I believe this affected certain Facebook users and not all. Specially, those that have been Facebook users around 2007 or so.
I’ve had my Facebook account since 2007 and I believe Mark Zuckerberg did too :)
Both, Zuk and I were affected because of this. I am sure there were others affected as well.
I’ll let you watch this video http://youtu.be/UFd68EG3E98 to show this in action.
It was as simple as clicking a hyperlink for the “BORN” highlight on your timeline. That would take you to a page that looks something like https://www.facebook.com/<user-id>/posts/<post-id>/. And, you would see yourself tagged with your parents.
This bug was worth $5000. I think this is a pretty generous amount for this bug. But, I am sure they rewarded this considering the ease of how this information could be leaked and the privacy violation for a lot of Facebook users.